feat(metadata): the ADR-0030 notification cut-over records its run in the sys_migration ledger, per the ruled claim matrix (#16100) - #16230
Conversation
…ation receipt Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ARYe3yQTQCUFm5qPYNgKaJ
…gration-receipt-row
Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ARYe3yQTQCUFm5qPYNgKaJ
…n tests and typecheck Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ARYe3yQTQCUFm5qPYNgKaJ
📓 Docs Drift CheckThis PR changes 1 package(s): 5 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 1 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 13 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 ab784512520b7d55dce2aa45c8604fa741d17a4f && git checkout ab784512520b7d55dce2aa45c8604fa741d17a4f
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin cbca47d09bc00b237f1f75046f03205dd4da2d32 387a3552bfdc212026af85bf1040f27ae04314e7 && git checkout -B drift-repro cbca47d09bc00b237f1f75046f03205dd4da2d32 && git merge --no-ff 387a3552bfdc212026af85bf1040f27ae04314e7
node scripts/docs-audit/affected-docs.mjs --json cbca47d09bc00b237f1f75046f03205dd4da2d32
|
Contract review (clause ②) — PASS on content · landable on green CI — PR #16230 at head
|
Fixes #16100
A completed run of
migrateSysNotificationToEventnow records itself in thesys_migrationdeployment ledger underNOTIFICATION_EVENT_MIGRATION_ID, exactly per the ruled claim matrix (maintainer 「同意」 to decision batch #47 item 5, carried on that constant's docblock in@objectstack/spec/systemsince #15710's spec half landed asa7da4de6f).Where the write went, and why the runner
The card offered
driver-exec.ts(the migration door) orserve.ts(the boot-time invocation) as the caller that owns the datastore handle. Triage measured both out, and I re-measured on my own base:sys_migrationoccurs 0 times in all ofpackages/metadatabefore this change — against a firing same-file control of 3exporthits indriver-exec.ts.driver-exec.tsis a 110-line raw-SQL entry-point resolver with two exports and no datastore handle.serve.ts's onlysys_migrationoccurrence is a comment aboutPlatformObjectsPluginregistering the ledger table, unrelated to this migration.The runner receives the data engine and is the only place that knows the four-valued outcome, so the receipt is written there. I re-confirmed by text (not line number) that the result union is still exactly
'migrated' | 'already_done' | 'not_applicable' | 'error'; a fifth member would have changed the matrix's arithmetic.The matrix, and how a fifth outcome is made to hurt
LEDGER_CLAIMis a mapped type keyed by the result union rather than a ternary, so adding an outcome makes the table a compile error instead of silently inheriting an arm:last_run_atapplied_atverified_atblockingdetailsmigrated0{"outcome":"migrated"}already_done0{"outcome":"already_done"}not_applicable0{"outcome":"not_applicable"}errorThe
verified_atandapplied_atcolumns are split by whether a row already exists: on INSERT they are spellednull(the documented receipt shape, and the absence of a certificate); on UPDATE the keys are omitted entirely. That is not a nicety. This id is inCREATION_ATTESTED_MIGRATION_IDS, so a store created after the cut-over already carries a row whoseverified_atattestFreshDatastoreset at birth, for a fact this run neither earned nor disproved — sending the column at all would forge that certificate or revoke it.applied_atgets the same treatment for the same reason in the other direction: an earliermigratedrun's stamp is a true fact and a lateralready_donemust not clear it.Receipt, not gate. No path consults a row under this id, and none is added here.
advisory,deviation_observed_atanddeviation_detailare not written: nothing here produces an advisory finding, and the deviation columns belong to ADR-0104's escape-hatch protocol this migration does not participate in.The published surface moved — clause ② is
yesClause-②: no. Re-derived from the diff, the answer isyes, and the claim comment on the card carries that declaration in the machine spelling.Writing the receipt introduces a failure mode the runner did not have: a claim that is owed and cannot land (no ledger on this host, or the write throws). No module under
packages/metadata/src/migrationstakes a logger — the directory reports to its caller — so the claim's fate is reported the same way the migration's is, in the returned result. That adds aSysNotificationMigrationReceipttype and a requiredreceiptmember onSysNotificationMigrationResult.Measured rather than judged. Built at head, swapped the two changed sources back to
origin/main, rebuilt, snapshotted, restored, rebuilt.npm pack --dry-runsays the package publishes 34 files, 10 of them declarations; 8 came back byte-identical and 2 differed (dist/migrations/index.d.ts,dist/migrations/index.d.cts), and the restore leg returned both to the head hash exactly. Every rebuild is proven to have re-run bydist/migrations/index.jsmtime moving on each leg, so the 8 identical hashes are a measurement and not a skipped build. Classifying the two hunks: declaration text, not JSDoc, not a content-hashed chunk name. Changeset isminor.Reading a result is unaffected. Code that CONSTRUCTS a
SysNotificationMigrationResultby hand now suppliesreceipt; the repo has no such site outside these tests.There is no operator-reachable run of this migration today: it has no production call site, and
os migratehas nonotification-eventsub-command (independently measured with a firing control by the #16067 round). A reviewer who assumes an operator path exists will mis-weigh the test coverage — until that changes, the cases below are the entire exercise surface, which is why every arm of the matrix is pinned separately rather than one happy path standing in for four.That gap is deliberately not folded in here: it would be an unruled CLI feature in another lane, and the PM seat is filing it separately.
Tests
13 new cases in
packages/metadata/src/migrations/migrate-sys-notification-to-event.test.ts— one per outcome, botherrorreturn sites (the pre-tryrefusal and thecatch), the fresh-store case in both directions,applied_atpreservation across a later run, bothno-ledgershapes, and thefailedwrite. The existing double gained an optional ledger rather than a new fake engine being introduced, so no new double enterscheck:engine-double-contract's ledger.The fresh-store row is seeded through the shipped producer
attestFreshDatastore, and the verdict is read back through the shipped readerisDataMigrationVerified— so the pin measures what a consumer would really see. Reaching those meant this package's tests and typecheck resolve@objectstack/platform-objects/systemtodist, whichcheck:test-source-aliasandcheck:type-source-resolutionboth reddened; the fix each gate prescribes (an anchored vitest alias, atsconfigpathsrule,rootDirwidened as the consequence) is in this PR. The tsc program grows 557 → 565 files, zero errors.Ablation. Moving
verified_atout of the insert-only branch so it is always sent turns the two fresh-store cases red (2 failed / 21 passed), and only those two. Mutation proven on disk before measuring — whole-line anchor, blobc30be0d1→499c7cf4; restore proven after — blob back toc30be0d1,git diff HEADempty, 0 residual markers, 23/23 green. No rebuild leg was owed: the suite imports the runner relatively, from source, so nodistis involved.Gates
Derived with
node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstackfrom the merged tree (never a hand-written list), re-derived after the config files entered the diff — the family was unchanged at 54.Its runnable answer is 54 families (43 by path + 6 by change kind + 7 declared whole-tree, 2 reached both ways). I ran all of them, plus the 10 declared-WIDE families the tool says no path can narrow — 64 invocations, every one exit 0, measured at
387a3552bwith redirect-then-read so the exit code is the gate's own and not a pipe's. The 6 value-bearing-argv families are ⊘ NOT MEASURED by construction: their values come from the workflow and the tool refuses to invent a local invocation.pnpm check:type-check-debtfirst returned exit 3 — PREREQUISITE NOT MET, a tsc OOM under my--max-old-space-size=4096cap, which the gate itself spells out is neither a pass nor a finding. Re-run at 8192 it is exit 0 (75/79 workspace packages type-checked, 4 in the DEBT ledger, 1 exempt) — that is the reading; the exit 3 is reported as NOT MEASURED, not as a failure.pnpm check:durability-log-levelis green, and its green is live on this diff rather than vacuous: swapping the runner back toorigin/mainmoves the read-seam population 68 → 67, so the new ledgerfind()is in the swept set and is judged as inventing no unreported answer. The write-side limb stays at 29 seams in both readings — its vocabulary is callee names and does not carry this one, which is the gate's declared narrowness, not a clearance.Suites:
pnpm --filter @objectstack/metadata test— 51 files / 792 tests, exit 0.pnpm --filter @objectstack/metadata typecheck— exit 0, and--listFilesputs all four edited source/test/config files in the program (565 files), so the green is about them.Generated by Claude Code