Skip to content

feat(metadata): the ADR-0030 notification cut-over records its run in the sys_migration ledger, per the ruled claim matrix (#16100) - #16230

Merged
zhuangjianguo merged 4 commits into
mainfrom
claude/issue-16100-migration-receipt-row
Sep 6, 2026
Merged

feat(metadata): the ADR-0030 notification cut-over records its run in the sys_migration ledger, per the ruled claim matrix (#16100)#16230
zhuangjianguo merged 4 commits into
mainfrom
claude/issue-16100-migration-receipt-row

Conversation

@claude

@claude claude Bot commented Sep 6, 2026

Copy link
Copy Markdown
Contributor

Fixes #16100

A completed run of migrateSysNotificationToEvent now records itself in the sys_migration deployment ledger under NOTIFICATION_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/system since #15710's spec half landed as a7da4de6f).

Where the write went, and why the runner

The card offered driver-exec.ts (the migration door) or serve.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_migration occurs 0 times in all of packages/metadata before this change — against a firing same-file control of 3 export hits in driver-exec.ts.
  • driver-exec.ts is a 110-line raw-SQL entry-point resolver with two exports and no datastore handle.
  • serve.ts's only sys_migration occurrence is a comment about PlatformObjectsPlugin registering 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_CLAIM is 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:

outcome last_run_at applied_at verified_at blocking details
migrated now now never written 0 {"outcome":"migrated"}
already_done now untouched never written 0 {"outcome":"already_done"}
not_applicable now untouched never written 0 {"outcome":"not_applicable"}
error no claim at all

The verified_at and applied_at columns are split by whether a row already exists: on INSERT they are spelled null (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 in CREATION_ATTESTED_MIGRATION_IDS, so a store created after the cut-over already carries a row whose verified_at attestFreshDatastore set at birth, for a fact this run neither earned nor disproved — sending the column at all would forge that certificate or revoke it. applied_at gets the same treatment for the same reason in the other direction: an earlier migrated run's stamp is a true fact and a later already_done must not clear it.

Receipt, not gate. No path consults a row under this id, and none is added here.

advisory, deviation_observed_at and deviation_detail are 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 yes

⚠️ The dispatch predicted Clause-②: no. Re-derived from the diff, the answer is yes, 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/migrations takes 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 a SysNotificationMigrationReceipt type and a required receipt member on SysNotificationMigrationResult.

Measured rather than judged. Built at head, swapped the two changed sources back to origin/main, rebuilt, snapshotted, restored, rebuilt. npm pack --dry-run says 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 by dist/migrations/index.js mtime 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 is minor.

Reading a result is unaffected. Code that CONSTRUCTS a SysNotificationMigrationResult by hand now supplies receipt; the repo has no such site outside these tests.

⚠️ These pins are the only thing that exercises this writer

There is no operator-reachable run of this migration today: it has no production call site, and os migrate has no notification-event sub-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, both error return sites (the pre-try refusal and the catch), the fresh-store case in both directions, applied_at preservation across a later run, both no-ledger shapes, and the failed write. The existing double gained an optional ledger rather than a new fake engine being introduced, so no new double enters check: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 reader isDataMigrationVerified — so the pin measures what a consumer would really see. Reaching those meant this package's tests and typecheck resolve @objectstack/platform-objects/system to dist, which check:test-source-alias and check:type-source-resolution both reddened; the fix each gate prescribes (an anchored vitest alias, a tsconfig paths rule, rootDir widened as the consequence) is in this PR. The tsc program grows 557 → 565 files, zero errors.

Ablation. Moving verified_at out 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, blob c30be0d1499c7cf4; restore proven after — blob back to c30be0d1, git diff HEAD empty, 0 residual markers, 23/23 green. No rebuild leg was owed: the suite imports the runner relatively, from source, so no dist is involved.

Gates

Derived with node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack from 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 387a3552b with 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-debt first returned exit 3 — PREREQUISITE NOT MET, a tsc OOM under my --max-old-space-size=4096 cap, 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-level is green, and its green is live on this diff rather than vacuous: swapping the runner back to origin/main moves the read-seam population 68 → 67, so the new ledger find() 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 --listFiles puts all four edited source/test/config files in the program (565 files), so the green is about them.


Generated by Claude Code

@github-actions github-actions Bot added size/l documentation Improvements or additions to documentation tests tooling labels Sep 6, 2026
@github-actions

github-actions Bot commented Sep 6, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/metadata, touching 20 documentable anchor(s). ⚠️ 2 changed file(s) yielded no anchor (packages/metadata/tsconfig.json, packages/metadata/vitest.config.ts), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

5 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/kernel/contracts/metadata-service.mdx (via getObject (symbol, a method of interface MigrationLedger), getObject (literal, a string literal in LEDGER_METHODS))
  • content/docs/permissions/authorization.mdx (via not_applicable (symbol, a field of const object LEDGER_CLAIM))
  • content/docs/permissions/explain.mdx (via not_applicable (symbol, a field of const object LEDGER_CLAIM))
  • content/docs/permissions/rls.mdx (via not_applicable (symbol, a field of const object LEDGER_CLAIM))
  • content/docs/protocol/objectql/state-machine.mdx (via /object/:name/state/:field (route, bridged from symbol getObject — its route source's handler names it))

1 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/v17.mdx (via getLegalNextStates (sdk, the bare tail of client method meta.getLegalNextStates, bound to GET /api/v1/meta/object/:name/state/:field; the bare tail of client method meta.getLegalNextStates, bound to GET /meta/object/:name/state/:field), meta.getLegalNextStates (sdk, the route ledger binds it to GET /api/v1/meta/object/:name/state/:field, selected by route anchor /object/:name/state/:field; the route ledger binds it to GET /meta/object/:name/state/:field, selected by route anchor /object/:name/state/:field))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • 2 changed file(s) yielded no anchor (packages/metadata/tsconfig.json, packages/metadata/vitest.config.ts) — pages documenting those are invisible to this run
  • 8 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 61 of 219 client-bound route-ledger rows — the other 158 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 158: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 56 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 102 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 13 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json cbca47d09bc00b237f1f75046f03205dd4da2d32packageMentionDocs.

Which tree this was computed on

This run read content/docs from ab784512520b7d55dce2aa45c8604fa741d17a4f — the merge of head 387a3552bfdc212026af85bf1040f27ae04314e7 into base cbca47d09bc00b237f1f75046f03205dd4da2d32, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# 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

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs cbca47d09bc00b237f1f75046f03205dd4da2d32 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

Copy link
Copy Markdown
Contributor

Contract review (clause ②) — PASS on content · landable on green CI — PR #16230 at head 387a3552 (Fixes #16100)

Reviewed by the director seat at tier (claude-fable-5-1, session session_01TezFG8ZMrNH6n5VTNpPpdH), 2026-09-06 07:04Z, on the domain:engine seat's hand-off (5557569494).

Clause ② answer: yes, mechanical floor — and correctly declared yes by the round after the seat's no. @objectstack/metadata/migrations gains an exported type SysNotificationMigrationReceipt and a required receipt member on the published SysNotificationMigrationResult; the seat's before/after build shows exactly dist/migrations/index.d.ts + .d.cts differing, 8 of 10 declarations byte-identical. Additive for every reader of a result; only a hand-constructed result (test doubles) must now supply receipt, and the repo has none outside these tests. minor on @objectstack/metadata is the right level, and no ADR-0087 marker is owed — nothing is removed or renamed.

Content against the ruling (decision batch #47 item 5, 「同意」): the matrix is implemented as a mapped type over the four-member status union, so a fifth outcome is a compile error, not an inherited arm. last_run_at on every non-error completion; applied_at only on migrated and never cleared; verified_at never written in either direction — null on INSERT, key omitted on UPDATE, which is what keeps attestFreshDatastore's birth certificate intact on a CREATION_ATTESTED_MIGRATION_IDS store; blocking: 0; details: {outcome}; an error run writes nothing and does not read the ledger. Receipt, not gate: no reader is added. The ledger seam is duck-typed the same way readDataMigrationFlag probes it, and the update(object, row, {context}) spelling matches IDataEngine.update(objectName, data, options) and the platform-objects writer at migration-flag.ts:197 — checked, since a wrong arity here would only fail against a real engine. recordNotificationEventReceipt never throws; a failed write comes back as receipt.outcome: 'failed' with the reason, which is the third legal answer to the degradation rule for a logger-less directory.

The two config edits (tsconfig.json paths + rootDir, anchored vitest alias for @objectstack/platform-objects/system) are the fix check:test-source-alias / check:type-source-resolution prescribe for driving the shipped attestFreshDatastore / isDataMigrationVerified from tests; dist layout is unaffected (34 packed files, measured by the round).

Tests read (migrate-sys-notification-to-event.test.ts, 407-line delta): one case per outcome, both error return sites, fresh-store both directions through the shipped producer and reader, applied_at preservation, both no-ledger shapes, the failed write.

Governed-merge audit on the 6 paths: 0 hits ⇒ ordinary landing. --pair 16230: the card's claim (5557181605) is in the fixed spelling. CI at 387a3552: 28 success · 6 skipped · 2 still running (Test Core (1/6), Lint & Repo Gates).

needs:contract-review comes off PR + card #16100 now. On green CI the next director pass re-runs --pair 16230 and flips ready-for-review + auto-merge (squash); a moved head is re-hung and re-read.


Generated by Claude Code

@zhuangjianguo
zhuangjianguo marked this pull request as ready for review September 6, 2026 07:15
@zhuangjianguo
zhuangjianguo added this pull request to the merge queue Sep 6, 2026
Merged via the queue into main with commit 8647c87 Sep 6, 2026
42 checks passed
@zhuangjianguo
zhuangjianguo deleted the claude/issue-16100-migration-receipt-row branch September 6, 2026 07:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/l tests tooling

Projects

None yet

3 participants