Repository navigation
refactor(metadata,spec)!: retire the adr-0030-notification-event migration — no operator door, no platform invoker - #17194
Conversation
…tire-notification-event-migration
…ation `migrateSysNotificationToEvent` had no way to be run: zero production callers anywhere in `packages` / `apps` / `examples`, and no `os migrate` sub-command, while the two sibling members of `CREATION_ATTESTED_MIGRATION_IDS` had both. Removed together: the runner and its three result/option/receipt types, its barrel export, its tests, the ruled `sys_migration` receipt-claim matrix and that matrix's pin, the spec docblock stating it, and the id's membership in `CREATION_ATTESTED_MIGRATION_IDS`. `NOTIFICATION_EVENT_MIGRATION_ID` is kept as the name of rows already written under it; nothing writes or reads one now. Both alternatives stay refused: an `os migrate notification-event` sub-command is a permanent operator surface for a migration with no measured demand, and a boot-time invoker is an unattended data rewrite. Pre-ADR-0030 `sys_notification` rows are not carried by the platform on this line. Two new retirement pins replace the withdrawn ledger pin, each asserting the two ADR-0104 survivors by name beside the negative, so a correct removal is distinguishable from a set that lost everything. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015QE8qk46e5CHJxyQEUjbf8
📓 Docs Drift CheckThis PR changes 3 package(s): 9 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 4 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 137 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin f9302b332f24409eeb37e9b368994792a835db09 && git checkout f9302b332f24409eeb37e9b368994792a835db09
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 0780e88481c302870b151aa40ef8d20935032d46 28eb180fdfe48d9333a3a1400c1e81da34b3384f && git checkout -B drift-repro 0780e88481c302870b151aa40ef8d20935032d46 && git merge --no-ff 28eb180fdfe48d9333a3a1400c1e81da34b3384f
node scripts/docs-audit/affected-docs.mjs --json 0780e88481c302870b151aa40ef8d20935032d46
|
…cription too The first pass tombstoned only the cut-over sequence's step 2. Three claims above it still described the migration as live: the `### Data migration (not auto-run)` section that names the deleted file and its export, the breaking-change callout telling a reader to "do the objectui cut-over and the data migration together", and the Tests line counting the migration's own suites among the green ones. Found by the docs-drift round on the PR. The runbook tombstone is now a pointer to the one full account, so the file states it once. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015QE8qk46e5CHJxyQEUjbf8
Contract review at
|
| # | Face | file:line (PR head) | Ruling artefact | Verdict |
|---|---|---|---|---|
| 1 | Runner deleted | packages/metadata/src/migrations/migrate-sys-notification-to-event.ts (−585) |
1 | correct |
| 2 | Barrel export removed, tombstone in place | packages/metadata/src/migrations/index.ts:49-77 |
2 | correct; rename control zero at head, fires at base |
| 3 | Tests removed | metadata runner test (−754), packages/runtime/src/notification-migration-audit-preservation.integration.test.ts (−282, #16312, migration-specific), two runner cases + engine double in real-driver-exec-surface.test.ts (−101) |
3 | correct; surviving three sibling cases untouched |
| 4 | Receipt row / ledger pin | packages/spec/src/system/notification-event-migration-ledger.pin.test.ts (−244; pinned only this id's matrix — 8 refs to it, siblings only as controls) |
4 | correct; the other two ids' docblocks (migration.zod.ts:148-170) untouched |
| 5 | Spec docblock → tombstone | migration.zod.ts:172-198 |
5 | correct; matrix absence pinned with a self-test control |
| 6 | Membership removed | migration.zod.ts:228-231 |
6 | correct; attestFreshDatastore (platform-objects/src/system/migration-flag.ts:315) reads the array ⇒ fresh stores now write 2 rows |
| — | NOTIFICATION_EVENT_MIGRATION_ID kept |
migration.zod.ts:199; still on api-surface/system.json:438 |
not enumerated | see open question below |
| — | CLI surface | packages/cli/** untouched; 0 hits for notification-event |
refused alternatives | correct — no sub-command, no boot invoker |
| — | engine-double-contract.pinned.json −6 rows |
regenerated via --write as the gate prescribes; no floor moved |
— | legitimate |
| — | content/docs/** |
0 paths; content/docs/releases/ 0 paths |
— | correct |
No silent widening: the re-fixtured #16067 case uses the pre-existing migrationIds option (migration-flag.ts:313), not a new one.
Open question (tier's to answer): keep vs delete NOTIFICATION_EVENT_MIGRATION_ID. Reading: keep (A). The ruling enumerates exactly six removals and names the docblock and the membership separately; deleting the constant would be a seventh removal of published surface the ruling did not name, and rows keyed by it exist on attested deployments. B needs its own ruling line, not a rider here. The it('the id itself is KEPT …') pin correctly makes A the pinned contract.
Findings
- F1 (low) — changeset omits
@objectstack/platform-objects..changeset/retire-adr-0030-notification-event-migration.md:1-4names metadata + spec only, but the operator-visible behaviour change (fresh datastore attests 2 rows, not 3) is delivered byattestFreshDatastorein platform-objects (private: false). The fixed group bumps it anyway, so only its CHANGELOG lacks the row. Note the ruling's execution paragraph itself named only metadata + spec, so this is advisory: add'@objectstack/platform-objects': minorif the seat wants the CHANGELOG to carry it. - F2 (low, governed follow-up) — stale ADR reference not covered by ADR-0030 still prescribes
migrateSysNotificationToEvent, a call the #16194 retirement removed — an operator copying step 2 gets an unresolvable import #17193.docs/adr/0052-audit-is-not-the-activity-feed.md:327still sayssys_notification"is mid-migration to an event model (metadata/.../migrate-sys-notification-to-event.ts, ADR-0030)". Governed, so correctly not touched here; ADR-0030 still prescribesmigrateSysNotificationToEvent, a call the #16194 retirement removed — an operator copying step 2 gets an unresolvable import #17193 names only0030:105. Fix: widen ADR-0030 still prescribesmigrateSysNotificationToEvent, a call the #16194 retirement removed — an operator copying step 2 gets an unresolvable import #17193 to include0052:327(0030:80is a shipped-history table row, no edit). - F3 (nit) — stale test comment.
packages/plugins/plugin-audit/src/read-audit-view-instant-preservation.integration.test.ts:19-28citesmigrate-sys-notification-to-event.test.tsand the deleted runtime integration test as living files. Reword to past tense in a follow-up; non-blocking. - F4 (nit, noted by dev, unfiled) — dead dependency.
packages/metadata/package.json:86keeps@objectstack/platform-objectsandvitest.config.ts:55keeps its alias with no importer left inpackages/metadata/src. Own PR; non-blocking.
Tests pin the contract
Spec pin (notification-event-migration-retirement.test.ts:125-157): literal two-member array by symbol and string, survivors by name, retired id absent by value, docblock matrix absent with a pre-retirement self-test that fires. Metadata pin (:55-110): module gone from disk with sibling files present, no importer under any spelling with a live sibling probe, barrel names absent with survivors present, tombstone present. platform-objects/src/system/migration-flag.test.ts:180-198: real attestFreshDatastore writes no row for the retired id and both survivors in the uniform shape, toHaveLength(2); plugin.test.ts boot cases follow the two-member set. Ablation A/B reported with predictions written first and matching (2 failed / 3 failed as predicted, tombstone/docblock controls green).
Docs
docs/handoff/adr-0030-notification-convergence.md:65-87,117-131 fully retired (section, breaking-change callout, tests line, cut-over sequence). content/docs grep on head: only releases/v17/17-4.mdx:376 names the id — a true record of 17.4.0, read-only, not falsified. AUTO-GEN references/system/migration.mdx does not state membership. No content/docs/releases/ edits.
Acceptance notes
Fixes #16194is the sole closing keyword and is correct (the ruled retirement is fully delivered; ADR-0030 still prescribesmigrateSysNotificationToEvent, a call the #16194 retirement removed — an operator copying step 2 gets an unresolvable import #17193 is referenced with zero verbs).- Live collision: open draft fix(types,metadata,cli): read the dialect text out of
causefor operator-facing records #17073 (card Raw-exec consumers that surfaceerror.messageas an operator-facingdetailnow read the composed DATABASE_ERROR sentence — readcausethere (follow-up to #16019) #16657) modifies the file this PR deletes (3-line hunk). Land this PR first; fix(types,metadata,cli): read the dialect text out ofcausefor operator-facing records #17073 then drops its hunk. Raised on fix(types,metadata,cli): read the dialect text out ofcausefor operator-facing records #17073 at5604919075. - PR-body measurements were taken at merge base
44c917a47before585dba94fmerged main; CI on the post-merge head is the reading and is green.
Generated by Claude Code
|
Landing provenance — director seat, summon #18 segment 6 ( Generated by Claude Code |
Landing window —
|
…igration Resolves the modify/delete conflict left by #17073, which added an `operatorFacingErrorText(err)` hunk to `packages/metadata/src/migrations/migrate-sys-notification-to-event.ts` — the module this branch deletes. The deletion is kept: the director-seat ruling of 2026-09-08 (decision batch #88) removes the runner, its barrel export and its tests in one PR, so #17073's hunk goes with the file it edits. Second, non-textual half of the same conflict: #17073 also added `packages/metadata/src/migrations/raw-exec-operator-detail-16657.test.ts`, which imports the deleted module and carries one describe block for it. That import and that block are removed — deletion only, no line authored; the three describe blocks covering the surviving migrations are kept untouched, as is every other file main brings. Claude-Session: https://claude.ai/code/session_015QE8qk46e5CHJxyQEUjbf8 Co-authored-by: Claude <noreply@anthropic.com>
Patch round landed, CI green, conflict gone — and one thing the tier should read before this mergesDispatching seat (
|
| suite | before | after |
|---|---|---|
| spec | 470 / 13191 | 470 / 13211 |
| metadata | 52 / 776 | 53 / 788 |
| platform-objects | 38 / 550 | 39 / 561 |
| runtime | 248 / 3511 | 251 / 3531 |
Nothing dropped anywhere; every delta traces to tests main brought. Typecheck green on all four. check:engine-double-contract exit 0 (848 pinned) — main never moved that ledger in this window, so ⛔ no --write and no hand edit. CREATION_ATTESTED_MIGRATION_IDS is still the 2-tuple; main added no third id. check:type-check-debt exit 3 = PREREQUISITE NOT MET, declared as NOT MEASURED, ⛔ not counted as a pass.
⚠️ Arming status, stated exactly rather than claimed
enable_pr_auto_merge was called at 02:17Z. Unlike the three PRs this seat armed earlier tonight, the API returned no merge method and no timestamp — and the tool documents failing gracefully when the PR is already mergeable, which this one now is (clean). So ⛔ do not read this comment as "it is queued": whether it entered the merge queue is unconfirmed at the time of writing and is being verified. If it did not, it stays green and clean and un-enqueued, and that is what the next seat inherits — ⛔ never merged outside the queue to work around it.
Owed at merge
The tier's four non-blocking findings: F1 the changeset could name @objectstack/platform-objects (the operator-visible "fresh datastore attests 2 rows, not 3" is delivered there); F2 docs/adr/0052-audit-is-not-the-activity-feed.md:327 still calls sys_notification mid-migration — governed, so it widens #17193 rather than being touched here; F3 a stale test comment in plugin-audit; F4 a dead @objectstack/platform-objects dependency in packages/metadata/package.json.
Also recorded from the round, deliberately not fixed: the docblock of raw-exec-operator-detail-16657.test.ts still says "at the three err?.message ?? String(err) sites" where two now remain, and main's own .changeset/operator-facing-raw-exec-cause-text.md lists the retired migration among those #17073 fixed. Both are past-tense narrative about what another PR did, no gate reads either, and correcting them would be authored lines a conflict-resolution round has no business adding.
Generated by Claude Code
|
Enqueue confirmed — closing the open question in the comment above. ⇒ Read the previous comment's arming caveat as resolved: #17194 is in the merge queue. Everything else in it stands — in particular the Generated by Claude Code |
|
Generated by Claude Code |
…esidue (objectstack-ai#18389) Fixes objectstack-ai#17281 Clause-②: no — this PR puts no new key on a published payload. It adds one changeset row, removes a dead workspace dependency (a narrowing, never a widening) and corrects stale prose; triage's grading `5692961190` states it directly: "nothing widens an accept set or a public surface". Declared by the dispatching seat in claim `5693760798`. Residue from the `adr-0030-notification-event` retirement (objectstack-ai#16194 / PR objectstack-ai#17194, merged `d64bcb6377`). Four items, four commits, item 1 first and alone as the triage grading `5692961190` directed, then re-cut on the maintainer's ruling `5698965201`. ## Item 1 — the `platform-objects` release note (re-cut on ruling `5698965201`) `797caf0c` first put `'@objectstack/platform-objects': minor` on the **existing** `.changeset/retire-adr-0030-notification-event-migration.md`. `d2544585` undoes that and takes route B instead. Two acts, one commit: - **The base note is restored byte-for-byte.** Measured, not asserted: `git rev-parse 8b81ab6:.changeset/retire-adr-0030-notification-event-migration.md` and `git hash-object` on the restored working-tree file both read `2ae652749b40dec974fdfb5fcf5fe0990267fe53`, and `git diff 8b81ab6 HEAD` on that path prints nothing. The file is no longer in this PR's diff at all. - **The bump moves to a new, issue-scoped changeset** — `.changeset/17281-platform-objects-attest-fresh-datastore.md`, declaring exactly `'@objectstack/platform-objects': minor`, named the way the gate itself prescribes (`.changeset/{issue}-{slug}.md`). The note is written for a release reader rather than for this PR: a datastore created from empty now attests **two** creation-attested migration ids instead of three, because `attestFreshDatastore` (`packages/platform-objects/src/system/migration-flag.ts`) defaults to `CREATION_ATTESTED_MIGRATION_IDS` and that tuple lost `'adr-0030-notification-event'` in the retirement. No row is written under the retired id any more; a deployment that already holds one keeps it, and `NOTIFICATION_EVENT_MIGRATION_ID` survives as that row's name. The bump is owed at all because that operator-visible half lives in `@objectstack/platform-objects`, a package with no `private: true`, and the retirement's own release note never declared it. ### The ruling, and its one declared cost Maintainer ruling `5698965201` chose **B**: open a new changeset file for `platform-objects`, and do not touch the release note that already exists on the merge base. That is the `confirmed` which `check:empty-changeset`'s DELIBERATE CORRECTION class asks for — and what it authorises is this route, not an edit in place.⚠️ The cost, named rather than hidden: the CHANGELOG will carry a **separate `platform-objects` entry** instead of a row under the adr-0030 retirement note. The same release semantics, told in two places instead of one. ### Two premises the ruling asked to be falsified before acting Both were put to a measurement that could have come out the other way. Neither broke. 1. **"Restoring the base note republishes a sentence that is now false."** It does not. Every claim in that note is about the retirement itself — the deleted runner, its three types, `CREATION_ATTESTED_MIGRATION_IDS` going from three members to two, `NOTIFICATION_EVENT_MIGRATION_ID` being kept. This PR's diff touches `packages/metadata` and `packages/plugins/plugin-audit` and **no** file under `packages/platform-objects`, `packages/spec` or `packages/metadata/src/migrations`'s deleted runner. The control that could have falsified it: grepping the base note for this PR's own subject matter — `platform-objects`, `dependen`, `vitest`, `tsconfig`, `package.json`, `alias`, `paths` — returns **zero** hits, so there is no sentence in it that this diff could have made false. The note is **under-declared**, not false: the `platform-objects` half moved in the original retirement (`d64bcb6377`, PR objectstack-ai#17194), whose file list carries `packages/platform-objects/src/system/migration-flag.ts`, `migration-flag.test.ts` and `plugin.test.ts`. 2. **"A changeset naming a package this diff does not move is refused by another gate."** It is not — measured for `minor` specifically, not inferred from the `patch` assertion. `check-changeset-no-major.mjs` run against a synthesised `pull_request` payload carrying this PR's real `Clause-②: no` exits **0**, printing `✓ LEVEL AXIS: this PR declares clause-② 'no'`. The firing control, the same command on the same tree with the payload flipped to `Clause-②: yes`, exits **1** — so the axis was live and capable of failing, and what it then prints is the sharper reading: the only package it lists is `@objectstack/metadata: patch`, because `packages/platform-objects/src/**` is not in this diff. A `minor` on a package the diff does not move is invisible to that gate in both directions — never refused, and never credited either. ##⚠️ Item 2 — the dead dependency (`bccdf6c1b`) `@objectstack/metadata` no longer declares `@objectstack/platform-objects`. Three files, removed together because all three exist only to serve one dead edge: the `dependencies` entry, the `@objectstack/platform-objects/system` alias in `vitest.config.ts`, and — **not named in the card, found while measuring** — the matching `paths` mapping in `tsconfig.json`, which pointed at the same dead specifier. The card required that no consumer resolving the dependency transitively be established **before** removing it. Established, not assumed: | reading | result | |:---|:---| | `@objectstack/platform-objects` specifiers under `packages/metadata` | **zero** in `src`, tests and scripts — only the three config/manifest sites removed here | | occurrences of `platform-objects` in `@objectstack/metadata`'s built `dist/` (30 files, 10 declaration files) | **zero** | | positive control: the nine other declared deps, same `dist/` | present in **4–12 files each** | | workspace packages carrying a real `platform-objects` module specifier | **23**, and **all 23 declare it themselves** | | of those, resolving it transitively | **zero** | | example apps referencing it | **zero** | ⇒ No runtime import and no type reference reaches `platform-objects` through anything `@objectstack/metadata` publishes, so no consumer can arrive at it via this package. `check:undeclared-dep-imports` is the standing gate that keeps the in-repo half true for non-test `src/**`, and it is green here. **Residual, declared rather than hidden:** an external consumer that installs `@objectstack/metadata`, imports `@objectstack/platform-objects` without declaring it, and relies on npm's flat hoist would lose that accidental resolution. That is unmeasurable from here; it is also the class this repo's own gate treats as a defect, and `@objectstack/platform-objects` publishes on its own, so the supported spelling exists. The changeset states the one-line fix. **`pnpm-lock.yaml` travels in the same commit**, regenerated with `pnpm install --lockfile-only` and never hand-edited. Its entire diff is the three lines of the one removed edge under the `packages/metadata` importer. An earlier push carried the manifest edit without it and failed `ERR_PNPM_OUTDATED_LOCKFILE` at every job's install step; that is fixed here, and it is ⛔ not evidence about whether the removal itself is safe — the readings above are. Graded `patch` — no export moves, no accept-set widens, no behaviour changes — and deliberately not `skip-changeset`: `package.json` is shipped by `npm pack`, and a consumer's install tree is what changes. ## Items 3 and 4 — the two stale prose lines (`466593dc0`) Comment-only; no assertion, harness or gate input moves. - `plugin-audit/src/read-audit-view-instant-preservation.integration.test.ts` cited `migrate-sys-notification-to-event.test.ts` and `packages/runtime/src/notification-migration-audit-preservation.integration.test.ts` as files a reader could open. Both measured absent, with a sibling that IS present as the positive control. Marked retired; the first citation is kept because the SHAPE it demonstrated is the paragraph's point. - `metadata/src/migrations/raw-exec-operator-detail-16657.test.ts` said "at the three `err?.message ?? String(err)` sites". **Two**, measured rather than carried: the directory holds two `catch (err: any)` sites recording through `operatorFacingErrorText` and one `catch (error)` site, matching the docblock's own two-expression split. The third is visible in the pre-merge tree of `d64bcb6377`, in the deleted runner, as `{ outcome: 'failed', reason: err?.message ?? String(err) }`.⚠️ **Commit shas corrected after the fact.** This body cited `063efde2` and `fd28ee6f`, and `git merge-base --is-ancestor` puts NEITHER on this branch (control: the merge base `8b81ab60d` exits 0 in the same checkout, so the negative is real). The branch was re-cut after that text was written. The live commits are `bccdf6c1b` (item 2) and `466593dc0` (items 3 and 4), each mapped by its own file list. Found by the delivering agent and corrected by the dispatching seat. ## Gates Derived from the actual changed files with `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` on head `d2544585e`, each run with its exit code written to disk **before** anything read it, then reconciled by feeding the codes back: ``` Run reconciliation — 75 derived, 75 run, 0 NOT-MEASURED, 0 UNRUN. ✓ dispatch-gates --ran: 75 derived famil(ies) accounted for — 75 run, 0 NOT-MEASURED (a DERIVED zero — all 75 recorded an exit code and none of them is 3). ``` **74 of 75 green**, plus `pnpm lint` — the full repo-wide union, which the derivation does not name and therefore never asks for — at **exit 0** on `d2544585e`. ⭐ **`check:empty-changeset` is the point of this commit, and it has turned over.** On `466593dc0` it exited **1**: ``` This PR changes a changeset it did not add: .changeset/retire-adr-0030-notification-event-migration.md present on the merge base and CHANGED by this PR ``` On `d2544585e` it exits **0**: ``` ✓ No empty-frontmatter changeset introduced by this diff (2 declaring changeset(s) added). ✓ No changeset from the merge base modified or deleted by this diff (objectstack-ai#17712). ``` `check-changeset-no-major.mjs` exits **0** on the same head, on both of its axes. The one family that is not green is not a defect introduced here: - **`check:cross-package-test-inputs` — exit 1, pre-existing (objectstack-ai#18353 / objectstack-ai#18348).** It names `@objectstack/cli` descending `packages/spec/dist/` from `packages/cli/test/init-created-files-summary.e2e.test.ts`; neither package is in this diff. Both readings were taken on this head: exit **0** before the build (that tree has no `packages/spec/dist`, so the walk had nothing to descend — a vacuous green) and exit **1** after it. The exit 1 is the reading that counts, and it is somebody else's card.⚠️ Six gates first read `exit 3` `PREREQUISITE NOT MET` — neither pass nor failure — because they load built output. After `turbo run build --filter=!@objectstack/docs` all six are green: `check:dts-closure`, `check:dual-build-cjs-loads`, `check:i18n`, `check:lean-entry-closure`, `check:sourcemap-no-sources-content`, `check:type-check-debt`.⚠️ The derivation printed a **STALE TREE** warning: this branch is at least 31 commits behind `origin/main`, and 10 of the files the families are derived from moved across that range. Discharged by measurement rather than by argument — the same derivation, run in a throwaway worktree checked out **at `origin/main` (`8fe5cb8e5`)** with this branch merged in uncommitted, derives the same 8 paths and a **byte-identical** 75-command list (`diff` exit 0). The staleness does not move the family set for these paths. Package-level test and typecheck readings are unchanged from `466593dc0` (`@objectstack/metadata` 53 files / 788 tests, `@objectstack/plugin-audit` 24 files / 346 tests, `typecheck` green for both): `d2544585e` adds nothing on top of that head but two `.changeset/*.md` files, which no test or typecheck program reads. ## Acceptance notes - `packages/metadata/vitest.config.ts` keeps one `platform-objects` mention, deliberately: the comment on the `@objectstack/spec` namespace rule contrasts spec's uniform export map with the FILE-shaped `@objectstack/platform-objects/plugin` subpath. That prose is still accurate and load-bearing about *spec*, and is not residue. - `.changeset/operator-facing-raw-exec-cause-text.md` is untouched — another PR's changeset, as the card directs. - `docs/adr/0052-audit-is-not-the-activity-feed.md:327` is untouched — governed surface, and objectstack-ai#17193 is its route. - Neither changeset this PR adds carries a `Clause-②` line, and the restored base note does not carry one either; 4 of the 334 changesets in the tree do. Noted, not filed — out of scope here. Claim: `5693760798`. Triage: `5692961190`. --- _Generated by [Claude Code](https://claude.ai/code)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
…d the retired migration (objectstack-ai#19381) Fixes objectstack-ai#17193 Clause-②: no ## The defect `docs/adr/0030-notification-platform-convergence.md` carried a live operator prescription for `migrateSysNotificationToEvent` — a runner the objectstack-ai#16194 retirement (decision batch objectstack-ai#88) removed from `@objectstack/metadata/migrations`. An operator following the cut-over list writes an import that does not resolve: a copy-the-example-and-it-fails defect, not a stylistic one. ## ⭐ The discrimination this card is about — two occurrences, ONE defect | occurrence | what it is | disposition | |---|---|---| | `0030` § *Remaining work* → objectui cut-over — "Run `migrateSysNotificationToEvent` during the cut-over…" | a **prescription addressed to someone about to act** | **corrected** | | `0030` § *Shipped (merged to `main`)* → `P0 — Seams` row — "…idempotent `migrateSysNotificationToEvent`. \| objectstack-ai#1434" | a **record of what a past release shipped**, true when written | ⛔ **left byte-for-byte as it stands** | **My reading of the file agrees with the card's split, and here is the structural evidence for it rather than a restatement.** The untouched occurrence sits inside the table under the heading `### Shipped (merged to `main`)` — one row per phase, each carrying the PR number that shipped it (`objectstack-ai#1434`). The corrected occurrence sits under `### Remaining work (handed off to a follow-up agent)`, in an imperative bullet list of steps a follow-up agent is told to perform. Retro-editing the shipped row would be rewriting history to make a grep pass, and it is the accrete-a-row-per-release pattern the release guardrail exists to stop. `git diff` on this branch contains **zero** hits for `P0 — Seams` — the row is not in the diff at all. ##⚠️ The widening request (`5611847419`) — **TAKEN**, and declared The `CONTRACT_REVIEW_TIER` verdict on PR objectstack-ai#17194 (finding F2) named a second line of the same class in a different ADR. The dispatching seat did **not** rule it in or out of scope and asked for the decision to be made out loud. **It is taken**, and the same test was applied to it first: - `docs/adr/0052-audit-is-not-the-activity-feed.md` § *6. Rollout* → *P0b* read "`sys_notification` **is deferred** — it **is mid-migration** to an event model (`metadata/.../migrate-sys-notification-to-event.ts`, ADR-0030), so moving it now would collide with that in-flight work." - Present tense, a claim about **what the platform does today**, and it names a path that is **deleted from disk**. That puts it on the `:105` side of the test, not the shipped-history side. Both halves of the sentence are false as of the retirement. **Why take it rather than file it.** It is the same defect, from the same retirement, in the same governed tree, and it needs the same sentence written. Leaving it means a second card, a second governed-surface PR and a second hand-merge for one sentence. The PM comment that recorded it declined to file it as a second card for exactly that reason. **What the correction deliberately does NOT do.** It records that the stated collision reason is gone — it does **not** decide whether the `sys_notification` ownership move now proceeds. That disposition is a decision for ADR-0052's own record owner, and the line says so in as many words. ADR-0052's **status line is not amended**: no decision of ADR-0052 moved, only one sentence of rationale was false. ## The one judgement call — ADR-0030's amended status line The card's "Suggested shape" asked for a decision on whether ADR-0030 wants an amended status line naming the retirement. **It does, and the PR writes one** (one line, appended to the existing `**Status**` line). Prime Directive objectstack-ai#13: reversing a recorded decision is itself a decision, and it needs a new ADR **or an amended status line on the old one**. The retirement already happened, elsewhere; because `docs/adr/**` is governed, PR objectstack-ai#17194 correctly could not carry the amendment, so this PR is where it is owed. Without it, the Status line still reads "**P0–P3b2 shipped**" with no trace that a shipped P0 item was withdrawn, and a reader who greps the status learns nothing. The wording follows the register's own precedents for this exact shape — ADR-0005 (`Amended (2026-08-09, objectstack-ai#6825 — the Phase-1 overlay-index migration is deleted…)`), ADR-0127 and ADR-0045 — and closes with an explicit "nothing else moves" so it cannot be read as a wider reversal.⚠️ **This is the line to strike first** if the seat or the maintainer wants a smaller diff: the `:105` correction stands on its own without it. ## Premise re-verified on this branch's base (`32708262d`) ``` $ git grep -nE 'migrateSysNotificationToEvent' -- . ':!*CHANGELOG.md' ':!content/docs/releases/' .changeset/retire-adr-0030-notification-event-migration.md:21:- `migrateSysNotificationToEvent` (`@objectstack/metadata/migrations`) — deleted. docs/adr/0030-notification-platform-convergence.md:80 (the shipped-history row — untouched) docs/adr/0030-notification-platform-convergence.md:105 (the prescription — this PR) docs/adr/0052-audit-is-not-the-activity-feed.md:327 (via its file path — this PR) docs/handoff/adr-0030-notification-convergence.md:126 (the tombstone, already correct) packages/metadata/src/migrations/index.ts:50 (barrel TOMBSTONE) packages/spec/src/system/migration.zod.ts:175 (retired id docblock) + three retirement pin tests ``` `packages/metadata/src/migrations/` holds no `migrate-sys-notification-to-event.ts`. Every remaining occurrence in shipped code is a tombstone or a pin asserting the absence. ⇒ the premise **holds**: the prescription names a call that no longer exists. ## Local gates — 19 derived, 19 run, 0 NOT MEASURED Derived with `node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands` against the actual changed files, every exit code captured **before** any pipe, reconciled with `--ran` in the `command :: exit code` form the tool asks for: ``` ✓ dispatch-gates --ran: 19 derived famil(ies) accounted for — 19 run, 0 NOT-MEASURED (a DERIVED zero — all 19 recorded an exit code and none of them is 3). ``` All 19 exited **0**, the ADR-specific ones among them: `check-adr-links.mjs` · `check-adr-symbol-anchors.mjs` · `check:adr-anchors` · `check:doc-authoring` · `check:pm-governed-merges` · `check:pm-prior-rulings` · `check:nul-bytes` · `@objectstack/lint check:doc-formula-expressions`. `check:doc-formula-expressions` first exited **3 — PREREQUISITE NOT MET** (`@objectstack/formula` and `@objectstack/lint` unbuilt). That is not a finding and was not recorded as one: the two packages were built and the gate re-run, where it exited 0. **Build / test scope.** The diff touches no package, so the dependency-closure build is empty and no package's `test`/`typecheck` is affected. The build above was a gate prerequisite only. **Repo-wide eslint — a measured narrowing, not a skipped run.** (1) Population, read from eslint's own config: every `files:` block in `eslint.config.mjs` matches only `{ts,tsx,mts,cts,js,jsx,mjs,cjs}`; no block matches Markdown. (2) Count, from `--format json` over both changed files: 2 files, both `File ignored because no matching configuration was supplied`, 0 errors. (3) Non-influence: no block sets `parserOptions.project` or `projectService`, so type-aware linting is not enabled and this diff cannot move the verdict on any untouched file. ## Changeset — `skip-changeset` is warranted, and is NOT applied by this PR The repo has a definite answer and it is the **label**, not an empty changeset: `AGENTS.md` *Post-Task Checklist* §3 scopes `skip-changeset` to "a diff that publishes nothing from any released package", and the empty-changeset route was ruled shut for new files (objectstack-ai#5471) — the `Check Changeset` job rejects an empty changeset a PR newly introduces. A docs-only `docs/adr/` diff publishes nothing from any package, so the label is the correct instrument. ⛔ **The dispatching seat's write budget names no label, so this dev did not apply it.** The `Check Changeset` gate will be red until the seat applies `skip-changeset`; that red is the gate working, not a finding. ## Files changed - `docs/adr/0030-notification-platform-convergence.md` — status-line amendment; the cut-over prescription struck and replaced. - `docs/adr/0052-audit-is-not-the-activity-feed.md` — the P0b rollout rationale corrected. Total: 2 files, +16 / −7. ## ⛔ Landing `docs/adr/**` is a governed surface (Prime Directive objectstack-ai#14, Tier H). This PR stays **draft** and takes no queue: it lands by the maintainer's hand or on an authorised approval, and no agent seat may flip it ready, enqueue it, arm auto-merge on it, or approve it. --- ## 维护者速读(草稿) > 席位意见一节留空,待席位填写后贴出终稿评论。 ### 改了什么 两个 ADR 文件里的两句话,共 2 个文件、+16 / −7 行,不含任何代码改动。 1. **ADR-0030**(通知平台收敛):「前端切换时运行 `migrateSysNotificationToEvent` 把历史铃铛数据迁过来」这条**操作指令**被划掉,改成记录「该迁移已撤回」,并指向 handoff 文档里已有的墓碑说明;切换步骤从三步变两步。同时在文件顶部的 `Status` 行补了一条 `Amended` 记录,写明这次撤回。 2. **ADR-0052**(审计不是动态流):P0b 那段说 `sys_notification`「正在迁往事件模型(某某文件,ADR-0030)」——这句话的两半现在都是假的(迁移已退役、文件已从磁盘删除)。改成记录「当初因此推迟」+「该理由已不存在」,但**不替记录所有者决定这次搬迁现在要不要做**。 3. ⛔ **ADR-0030 里另一处同名调用(P0 已交付表格第 80 行)一个字没动** —— 它记录的是 objectstack-ai#1434 当年确实交付了什么,是历史,不是现状。 ### 为什么改 被点名的那个函数在 objectstack-ai#16194(决策批次 objectstack-ai#88)里已经从 `@objectstack/metadata/migrations` 删掉了。ADR-0030 那条指令是写给「马上要动手的人」看的:照抄它写出来的 import 解析不了。这属于「照着例子做就失败」,不是措辞问题。 ADR-0052 那句的危害不同但同源:它给一次**推迟**提供理由,而那个理由已经蒸发;后来的人可能继续拿一条死掉的理由往后推。 顶部 `Status` 行那条 `Amended` 是协议第 13 条要求的:推翻一个已记录的决定本身就是决定,必须记在记录上。撤回发生在别处(objectstack-ai#16194),而那个 PR 因为受管面规则不能碰 ADR,所以这笔账落在本 PR。 ### 风险与代价(含回滚) - **风险很低**:纯文档散文改动,无代码、无发布面、无生成物。19 个派生门禁全部 exit 0(含 ADR 链接、ADR 锚点、受管面合并审计)。 - **唯一需要您拍板的**:顶部那条 `Amended` 状态行算不算「越界替记录所有者做决定」。本 PR 的判断是「不算 —— 它只是把别处已经做出的裁定记到该记的地方」,但如果您觉得 diff 该更小,**先划掉这一行**,下面 `:105` 的修正独立成立。 - **回滚**:`git revert` 一个 commit 即可,不牵动任何运行时。 ### 席位意见 (留空) ### 你要做的 读完 diff 的 16 行后,**人工合并本 PR**(或给出授权批准)—— ⛔ 它不进合并队列,agent 席位不会翻 ready、不会入队。另外请给它挂上 `skip-changeset` 标签,否则 `Check Changeset` 会一直红(本 PR 不发布任何包,派发席的写预算里没有标签,所以 dev 没有自行挂)。 --- _Generated by [Claude Code](https://claude.ai/code/session_01NcPSwnmJHczmTu6FG7NMjE)_ Co-authored-by: Claude <noreply@anthropic.com>
Fixes #16194
Clause-②: yes
Executes the director-seat ruling of 2026-09-08 (decision batch #88, comment
5582372148) — the third answer. ⛔ Both alternatives stay refused and this PR adds neither: noos migrate notification-eventsub-command, no boot-time invoker.The ruling, verbatim (Chinese decisions quoted in the original elsewhere; this one is English):
Clause-②: yesfires on both limbs — an exported symbol and an attested id are removed, andpackages/spec/src/system/migration.zod.tsis a*.zod.tsunderpackages/spec/src/**(a content-independent limb). A clause-② card builds at the default judgment tier and is reviewed atCONTRACT_REVIEW_TIER; the enqueue gate refuses the queue until an in-seat PASS is on record. That tier is measured unavailable to the dispatching session. ⇒ this PR is meant to sit here, green, until that changes. ⛔ Not a failure, ⛔ do not flip it ready, ⛔ do not arm auto-merge, ⛔ do not work around the label.The six artefacts, each proved removed rather than renamed
Measured before/after (
git grepat merge base44c917a47vs branch head), excluding CHANGELOGs and release notes:migrateSysNotificationToEvent(the runner)migrate-sys-notification-to-event.tspresentpackages/metadata/src/migrations/index.ts⚰️ TOMBSTONEblock in the house style of theaddSysMetadataOverlayIndextombstone above itmigrate-sys-notification-to-event.test.ts(754 lines) andnotification-migration-audit-preservation.integration.test.ts(282 lines) present; 5 references inreal-driver-exec-surface.test.tsreal-driver-exec-surface.test.ts(its two runner cases and the now-unused engine double went with them)sys_migrationreceipt row for this idnotification-event-migration-ledger.pin.test.ts(244 lines) pinning the ruled matrix; 7DATA_MIGRATION_FLAG_OBJECTsites in the runnerNOTIFICATION_EVENT_MIGRATION_IDCREATION_ATTESTED_MIGRATION_IDSNOTIFICATION_EVENT_MIGRATION_IDFILE_REFERENCES_MIGRATION_ID,VALUE_SHAPES_MIGRATION_IDRename control, both legs.
git grep -nE 'export [^;]*(SysNotificationMigration|migrateSysNotificationToEvent|NotificationToEvent)'returns zero at the branch head. The same expression at the merge base returns the four declarations (SysNotificationMigrationReceipt,…Result,…Options,migrateSysNotificationToEvent) — so the probe fires and the zero is a reading, not a dead grep.NOTIFICATION_EVENT_MIGRATION_IDitself is kept, deliberately: the ruling enumerates the docblock and the membership as separate removals, which it would not need to do if the constant were going. A deployment attested at birth still holds asys_migrationrow keyed'adr-0030-notification-event', and the constant is that row's name. Nothing writes or reads one now. See Open question below.Acceptance conditions
① The population, re-measured with a firing control
Re-run on the merge base, not on the card's older head:
docs/and*.test.ts, every occurrence was the definition, its own usage docblock, the barrel line, one comment indriver-exec.tsand the id's docblock in@objectstack/spec. The only two call-shaped occurrences were line 27 (inside a docblock,* await migrateSysNotificationToEvent(...)) and line 168 (the definition).packages/runtime/src/notification-migration-audit-preservation.integration.test.ts(The sys_notification migration back-dates created_at through the create-side ?? that #15964 removes, so migrated inbox rows and receipts get the migration instant #16312) landed since. A run finding zero everywhere would be a dead grep; this one found 17 files and 42 non-CHANGELOG occurrence lines while finding zero production callers.② The three sibling ids are the control
Two new pins, and every negative in them is paired with the survivors asserted by name — a case that only says "the third is gone" cannot tell a correct removal from a set that lost everything:
packages/spec/src/system/notification-event-migration-retirement.test.ts— the array is read as a literal, in order, twice (by symbol and by string), plustoHaveLength(2); both ADR-0104 ids asserted present by name; the retired id asserted absent by value.packages/platform-objects/src/system/migration-flag.test.ts— a new case runs the realattestFreshDatastoreand asserts no row and no attestation for the retired id, and that both survivors still get their row in the uniform shape (applied_at: null,blocking: 0,details.attested,verified_atset) and read back verified.③ Removal, not rename
The table above, plus the two-legged rename control.
④ Ablation — directions predicted in writing first
Predictions were written into the ablation script before it ran (
ablate.shheader). Both matched exactly:A — put the id back into
CREATION_ATTESTED_MIGRATION_IDS.Predicted: RED on exactly two cases in the spec pin, and the two docblock cases stay GREEN (a pin that reddened on both would not be telling the artefacts apart).
Observed:
vitest exit = 1,2 failed | 5 passed—× CREATION_ATTESTED_MIGRATION_IDS is exactly the TWO surviving ids, in order,× the retired id is not a member, by value. Docblock cases green.B — put the runner module and its barrel export back.
Predicted: RED on exactly three cases in the metadata pin, and
the barrel carries a tombstonestays GREEN (the tombstone is left in place, so that case discriminates the export from the prose beside it).Observed:
vitest exit = 1,3 failed | 1 passed—× the module is gone from disk,× nothing in this package imports or re-exports the module, under any spelling,× the barrel exports no name derived from it. Tombstone case green.Mechanics. The fix was committed before either leg. Each mutation proved on disk by occurrence counts and a blob hash off the HEAD blob — A: membership lines
0 -> 1, blobe997213…to75c399c…; B: barrel export blocks0 -> 1, blob5e42483…to23da44c…, and the restored runner byte-identical to the merge-base blobdc21ee6…. Each restore proved bygit checkout HEAD -- path(⛔ never a baregit checkout --, which takes the index), a blob equal to HEAD's, and an emptygit diff HEAD; the script ends by observinggit status --porcelainis empty. An empty hash is treated as a FAILURE, never as "nothing to compare". The whole thing runs undertrap … EXIT INT TERMwith absolute paths only.Not a dist ablation, stated rather than assumed: both subjects are read through relative, in-package specifiers (
./migration.zod,./index.js) that vitest resolves tosrc/, never through a packageexportsentry todist/, so there is nothing forablation-dist-preflight.mjsto assert and no rebuild leg. Theplatform-objectssuite does reach spec throughexportsand is deliberately not the suite ablated, for exactly that reason.⑤ Single-writer holders, from the OPEN PR LIST
Measured from
GET /pulls?state=openand each PR's own/files(its head against its own merge base) — ⛔ never from remote branches. My authored face is the 16 paths ofgit diff --name-only $(git merge-base origin/main HEAD) HEAD, not the three-dot set against a stale base, which would have counted main's own merged commits as mine.causefor operator-facing records #17073 (fix(types,metadata,cli): read the dialect text out of cause for operator detail, issue Raw-exec consumers that surfaceerror.messageas an operator-facingdetailnow read the composed DATABASE_ERROR sentence — readcausethere (follow-up to #16019) #16657) also writespackages/metadata/src/migrations/migrate-sys-notification-to-event.ts— the file this PR deletes. Its change there is 3 lines: anoperatorFacingErrorText(err)swap inside the runner'scatch. Whichever lands second resolves a delete/modify conflict; the ruling decides the direction.packages/cli/**norpackages/client/src/index.tsis on it.scripts/engine-double-contract.pinned.jsonhad a holder (fix(plugin-security): stop letting org-admin row count decide whether a platform admin already exists #17116) at the first measurement; it has since landed, and its row survives the merge here alongside this PR's removals — asserted by grepping for both.packages/services/service-analytics/src/plugin.ts(a path an open PR really holds) into the same join makes it fire, naming PR feat(service-analytics)!: refuse an aggregate a datetime measure's field type cannot carry, and reconcile the storage-form annotations to one measured statement #16778. The instrument works.Coordination with #16100 — verified rather than assumed
The ruling predicted #16100 was "in flight". It is not: #16100 closed
completedon 2026-09-06, two days before the ruling. What it actually shipped is the receipt writer inside the runner plus the#15710ledger pin — both of which this PR removes, which is what the ruling names as artefact 4. The receipt-writing machinery it added was specific to this id, not generic: it lived inmigrate-sys-notification-to-event.tsitself. Nothing generic is lost, and #15710's matrix keeps its other two ids untouched.Reversal path
If a named deployment turns out to hold pre-ADR-0030 notification rows it needs, the migration returns as an operator-runnable
os migratesub-command shaped exactly likefiles-to-references/value-shapes— dry-run default,--applygate, documented consequence, all three of which this migration never had — under its own card. ⛔ Not as a quiet reintroduction. This is recorded in three places that outlive the PR: the changeset, the barrel tombstone, and the id's docblock.Nobody has measured whether any live deployment — cloud included — carries pre-ADR-0030
sys_notificationrows. The basis for not weighting that is the maintainer's standing instruction 「2069 不考虑现有数据」, quoted in the original per AGENTS.md. This PR does not present the retirement as if that measurement had been made. Concretely: a deployment that still holds legacy per-user inbox rows keeps them where they are, and the new bell will not show them.Verification
Every exit code captured by redirect-then-
$?, ⛔ never through a pipe.Gate family — derived with
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackon the final tree (⛔ never a hand-written list), then reconciled:85 of 88 pass. 3 are NOT MEASURED, declared as such — each exits 3, its own PREREQUISITE-NOT-MET code, distinct from a finding's 1, because it reads built output for the whole workspace and this box has only the four affected packages built. ⛔ Not counted as passes:
pnpm check:dual-build-cjs-loads—PREREQUISITE NOT MET — this gate reads built output, and some package has no dist/(37 packages named).pnpm check:i18n— needs the CLI plus the build closure of every package whose extract config it runs.pnpm check:type-check-debt—--re-measure cannot run: 1 workspace dependenc(ies) … have no built type entry point on disk. Its own text: this is NOT a pass and NOT a finding.CI builds the whole workspace and will measure all three.
Tests and typecheck, on the final head:
pnpm --filter @objectstack/spec test470 passed (470)files,13191 passed (13191)pnpm --filter @objectstack/metadata test52 passed (52)files,776 passed (776)pnpm --filter @objectstack/platform-objects test38 passed (38)files,550 passed (550)pnpm --filter @objectstack/runtime test248 passed (248)files,3511 passed (3511)typecheck(spec, metadata, platform-objects, runtime)tsconfig.test.jsonpnpm --filter @objectstack/spec check:generated✓ All 15 generated artifacts are up to date— includingcheck:api-surface,check:authorable-surface,check:docs,check:export-originsscripts/engine-double-contract.pinned.jsonwas regenerated withnode scripts/check-engine-double-contract.mjs --write, exactly as the gate's own refusal prescribes for a deleted test. The diff is 6 rows removed, 0 added — 3 from the deleted runner test, 3 fromreal-driver-exec-surface.test.tswhose engine double is genuinely gone (that file no longer needs an engine at all). Gate green after.pnpm check:nul-bytesgreen, plus a hand sweep of the diff withgrep -naP '[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]'.Docs-drift round — discharged
Nothing in
content/docsis falsified by this PR, and nothing there is edited. This PR touches zerocontent/docspaths. Every listed page is an input row: it mentions a symbol or literal the diff touches, not a page the diff changes.Closing the bot's own uncommitted-checkout hole, by tree object rather than by sha
The bot's merge tree
c6f329fdfcis not fetchable here, so the comparison is anchored on its base commitfb7d75fca1, whosecontent/docstree isf5fe6bec53…— identical toorigin/main's. My head'scontent/docstree is150e82d6e9…. The two differ in exactly one file,content/docs/permissions/system-context.mdx, and it is main's: the commit that changed it isfb7d75fca1itself (the bot's own base), whichgit merge-base --is-ancestoranswers is not on this branch (exit 1), with a control leg —fd5cff209f, known to be in this HEAD's history — answering exit 0 in the same shallow checkout, so the negative is a reading rather than a truncation artefact.git diff --name-only $(git merge-base origin/main HEAD) HEAD -- content/docsreturns 0 paths.Re-derived with
node scripts/docs-audit/affected-docs.mjs --json fb7d75fca1c45dc9bfa5875df8eec6b385a2efcb: same 13 docs, and"dirty": falseon this checkout — this re-derivation has no uncommitted-changes hole.The release-owned rows — ⛔ read only, ⛔ not edited
⭐
content/docs/releases/v17/17-4.mdxis NOT falsified. Two rows name the surface (plus a third the round did not list)::376— "a7da4de—adr-0030-notification-eventjoinsCREATION_ATTESTED_MIGRATION_IDS…":226— "8647c87— A completed run of the ADR-0030 notification cut-over now records itself in thesys_migrationdeployment ledger …":382— "c2a336c—@objectstack/spec/systemnow names the ADR-0030 notification cut-over …"Why they stand: the page is the record of a version that is already out.
@objectstack/spec@17.4.0is published and is npm'slatesttag; the repo is at17.4.0andpackages/spec/CHANGELOG.mdcarries its## 17.4.0section. This PR's changeset isminor, so its removal lands in 17.5.0 — a version with no page yet. Each row is a true statement about what 17.4.0 shipped, and 17.4.0 did ship exactly that.:382is doubly safe:NOTIFICATION_EVENT_MIGRATION_IDis kept, so@objectstack/spec/systemstill names the cut-over today. Retro-editing a shipped release page is precisely what thecontent/docs/releases/guardrail exists to prevent; this PR's input to release notes is its changeset, which already states the removal.The other three release rows are wrong rows:
17-0.mdx:384—sys_notification.action_urlcited as evidence for a flow-node key rename. It describes the object's column, which exists; anchored via thesys_notificationstring literal that the deleted runner'sEVENT_OBJECTconst held.17-0.mdx:756and17-2.mdx:240/:375—meta.getLegalNextStates, reached through the SDK route bridge. Zero relation to the migration.17-1.mdx:172— localized templates reachingsys_inbox_message: the live pipeline, which this PR does not touch.The nine hand-written rows — judged, and most are wrong rows
permissions/authorization.mdx,permissions/explain.mdx,permissions/rls.mdxnot_applicablenot_applicableas an authorization layer verdict in the closed vocabularygrants/denies/narrows/widens/neutral/not_applicable. The anchor is the migration result's four-valued vocabulary (migrated/already_done/not_applicable/error). A bare-token collision, matched on the string and not on its vocabulary.kernel/contracts/metadata-service.mdxgetObjectLEDGER_METHODS = ['getObject', 'find', 'insert', 'update']and a method ofinterface MigrationLedger— both private, non-exported declarations that lived inside the deleted runner as a duck-typing probe. The page documentsIMetadataService.getObject(name), a different declaration in a different package, untouched.protocol/objectql/state-machine.mdxgetObjectGET /api/v1/meta/object/:name/state/:field.protocol/kernel/realtime-protocol.mdxrecipient_idfromLEGACY_COLUMNS### Case 3: Live Notifications, an illustrative WebSocket example subscribing to a genericnotificationobject. The page never mentionssys_notificationor any migration (grep: zero hits).LEGACY_COLUMNSwas a private const naming the pre-ADR-0030 columns the runner cleared; deleting it changes no object shape and no column.automation/hook-bodies.mdxsys_notificationsys_notification, an object that still exists.concepts/north-star.mdxsys_notification_receiptsys_notification_receipt." About the live model; still true.api/client-sdk.mdxsys_inbox_message/sys_notification_receiptsys_inbox_messageand tracks read-state insys_notification_receipt, helpers to be repointed at the bell cut-over. All still true.The bot's declared coverage hole, searched by hand
packages/metadata/src/migrations/driver-exec.tsandpackages/platform-objects/src/system/migration-flag.tsyielded no anchor. Searched every exported name of each acrosscontent/docsanddocs:driver-exec.ts—driverExecRefusal,resolveDriverExec,driverCanRunSql,DriverExec: zero pages each. Its documented surface is empty, and this PR's change to it is one doc-comment paragraph.migration-flag.ts—attestFreshDatastore,readDataMigrationFlag,isDataMigrationVerified,recordDataMigrationRun,CREATION_ATTESTATION_DETAIL,MigrationFlagEngine, plus the prose termscreation-attestedandcreated-empty: two hits, both indocs/adr/0104-…— afile#symbolpointer tomigration-flag.ts#attestFreshDatastore(the function still exists, same name, same file) and the phrase "every datastore creation-attested since 17.0" in a section about media-column encoding. Neither states the attested set's cardinality nor namesadr-0030-notification-event(grepfor both: zero). Not falsified.Positive control for the hand search:
sys_migrationreturns 8 pages, so the search reaches the docs tree. Of those,deployment/seed-tenancy-repair.mdx:126is the seed-tenancy repair's own row (a different id, untouched) andreferences/system/migration.mdxis AUTO-GEN, current —check:docsis green insidecheck:generated.⭐ The input-vs-emitter blind spot, searched as a prose class
The bot cannot see a page that says "the platform migrates your old notification rows" without naming a symbol. Swept
content/docsanddocsfor that claim class in both orders (migration verb near a notification-row noun, and the reverse), with a positive control that fires on the two documents known to carry the claim (docs/handoff/adr-0030-…anddocs/adr/0030-…). Three hits outside those:references/automation/schemaless-node-config.mdx:159— aboutscript.config.templateandsys_notification_template. Unrelated, and an AUTO-GEN references page.concepts/north-star.mdx:129— the live read-state model, judged above.docs/design/notification-platform-convergence.md— a page the anchor-based run never listed, because it names no symbol of mine. Its §5 "Open decisions (ADR-0030 recommends; confirm before P0)" recommends "sys_notificationrename/re-model in place + migration", and §6 Risks says P0 "migrates a live, UI-depended object". Judged historical, not falsified, not edited: it is a dated build spec whose own status block (2026-06-01) declares P0 shipped and routes the reader to ADR-0030 and the handoff doc for current status — and the handoff doc is the one this PR corrects. Editing a superseded plan record to reflect a later retirement is the accrete-a-row-per-PR pattern. Reported rather than actioned.docs/handoff/adr-0030-notification-convergence.mdwas incompletely retired on the first pass and is now complete. The first edit tombstoned only the cut-over sequence's step 2; three claims above it still described the migration as live:### Data migration (not auto-run)section naming the deleted file path and its export — now### ⚰️ Data migration — RETIRED, there is none, carrying the reasoning, the unmeasured-deployment caveat and the reversal path;The runbook tombstone is now a pointer to that one full account, so the file states it once.
grep -ni 'migrat'over the file returns only past-tense and tombstone lines.pnpm check:doc-authoring,node scripts/docs-audit/check-affected-docs.mjs,node scripts/docs-audit/check-drift-comment.mjsandpnpm check:nul-bytesall exit 0 after the change.scripts/engine-double-contract.pinned.json−30 — the polarity, stated$commentsays so: "This is the OPPOSITE polarity toengine-double-contract.baseline.json: that ledger records DEBT and may only shrink, this one records COVERAGE and may only grow. A row that disappears is a pinned double that left the population." So a decrease is the flagged direction here, and it is the direction this diff moves — 6 rows, 30 lines, 0 added.It is legitimate and it is the path the gate itself prescribes. The gate refused first, naming each row and saying: "A deleted test is a LEGITIMATE decrease: run
node scripts/check-engine-double-contract.mjs --writeand commit the ledger. There is no number to choose and no judgement to make — the diff records which pin left." That is exactly what was done. Three rows leave with the deletedmigrate-sys-notification-to-event.test.ts; three leave becausereal-driver-exec-surface.test.tsno longer declares an engine double at all — itsrecordingEngine()existed solely for the one helper that needed anIDataEngine, and that helper is gone, which is the "genuinely and intentionally gone" case the gate's text reserves for--write. ⛔ Not a baseline relaxation: no floor moved and no threshold was edited. The gate re-runs green atpinnedRows 774against a floor of600, and its provenance line reports the population moving in both directions across the tree, which it states is "information, not a verdict — only the floors decide."验收备注
docs/adr/0030-notification-platform-convergence.md:105still says "RunmigrateSysNotificationToEventduring the cut-over". ⛔ Deliberately not touched here:docs/adr/**is a governed surface, one path hit would make this whole diff hand-merge-only, and amending an ADR is itself a decision (Prime Directive [WIP] Add Chinese version of the documentation #13). Filed as ADR-0030 still prescribesmigrateSysNotificationToEvent, a call the #16194 retirement removed — an operator copying step 2 gets an unresolvable import #17193. The operator-facing half was fixed, because it is not governed:docs/handoff/adr-0030-notification-convergence.md— the runbook ADR-0030 defers to — now carries a tombstone where its step 2 was, naming the reversal path. The doc an operator is actually sent to is correct today.@objectstack/metadatastill declares@objectstack/platform-objectsindependencies, and nothing inpackages/metadata/srcimports it any more — the deleted runner was its only consumer, along with the@objectstack/platform-objects/systemalias inpackages/metadata/vitest.config.ts(whose comment cites metadata: theadr-0030-notification-eventmigration writes itssys_migrationreceipt row per the ruled ledger-claim matrix —last_run_aton every completed run,applied_atonmigratedonly, neververified_at(#15710 follow-on) #16100). Noted, not filed: dead configuration is not one of the three fileable classes, and dropping a published package's dependency is its own public-surface change with its own risk. Taker: the next PR touching that manifest's dependency block.packages/platform-objects/src/plugin.test.tshad the three ids hard-coded in four places andtoHaveLength(3)in three; all now follow the two-member set. The case for a boot whose seed contradicted the file id previously expected['adr-0104-value-shapes', 'adr-0030-notification-event']and now expects the single survivor.#16067no-remedy pin was re-fixtured, not deleted. Its subject was the retired id — the one member with no row inVALUE_SHAPE_CONTRACT_REMEDY. Every surviving member has a remedy, so the array can no longer supply that case with a lookup miss, and the miss is the whole branchattestFreshDatastorenamesos migrate value-shapesas the remedy for every contradicted id that is notfiles-to-references— with a third creation-attested id the ternary is a wrong remedy on a path nothing reaches yet #16067 landed. It now hands in a synthetic id throughmigrationIds, and asserts up front that the id is not a member of the real array — otherwise the case would silently stop testing the branch it names.check-adr-0087-registrationrefusesnot-required (no-migration-prescription)when the body carries a rewrite prescription, and the first draft's| FROM | TO |table tripped it — correctly, since its TO column read "removed", which is not a TO. Nothing here is renamed and nothing replaces anything: the consumer action is a deletion, delivered by the compiler on the import. The disposition the ruling named is the one on the changeset, and the gate now passes on a positive reading rather than a detector miss.Open question for the review tier
Should
NOTIFICATION_EVENT_MIGRATION_IDitself be deleted? This PR keeps it, on the ruling's own wording: it enumerates "the spec docblock" and "the id's membership" as two separate removals, which would both be implied by — and so redundant with — deleting the constant, and the acceptance conditions likewise count six artefacts with the constant not among them. Keeping it also keeps a name for thesys_migrationrows fresh stores were already attested with. The counter-argument is Prime Directive #10's corollary: a registered migration id whose migration cannot run is a declaration the runtime does not deliver. Reversing this is a two-line change (drop the constant, drop theit('the id itself is KEPT …')case) if the tier reads the ruling the other way.Generated by Claude Code