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 cd301e4d7e2f2a818acc2cf333790a1776c2cdff && git checkout cd301e4d7e2f2a818acc2cf333790a1776c2cdff
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin c3756ff096f8bd4e437e36152779dafe0e7cf41a 12bc827ac47487b035c04248e884a476e98f0004 && git checkout -B drift-repro c3756ff096f8bd4e437e36152779dafe0e7cf41a && git merge --no-ff 12bc827ac47487b035c04248e884a476e98f0004
node scripts/docs-audit/affected-docs.mjs --json c3756ff096f8bd4e437e36152779dafe0e7cf41a
|
…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
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