Skip to content

refactor(metadata,spec)!: retire the adr-0030-notification-event migration — no operator door, no platform invoker - #17194

Merged
os-project-manager merged 8 commits into
mainfrom
claude/issue-16194-retire-notification-event-migration
Sep 10, 2026
Merged

refactor(metadata,spec)!: retire the adr-0030-notification-event migration — no operator door, no platform invoker#17194
os-project-manager merged 8 commits into
mainfrom
claude/issue-16194-retire-notification-event-migration

Conversation

@claude

@claude claude Bot commented Sep 9, 2026

Copy link
Copy Markdown
Contributor

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: no os migrate notification-event sub-command, no boot-time invoker.

The ruling, verbatim (Chinese decisions quoted in the original elsewhere; this one is English):

Ruled. The third answer. A migration nothing can run is not kept alive by three landed artefacts around it: migrateSysNotificationToEvent, its barrel export, its tests, the sys_migration receipt row for this id (#15710's matrix keeps the other two ids), the spec docblock and the id's membership in CREATION_ATTESTED_MIGRATION_IDS are removed in one PR. Pre-ADR-0030 sys_notification rows are not carried by the platform on this line.


⚠️ This PR is expected to be parked outside the merge queue

Clause-②: yes fires on both limbs — an exported symbol and an attested id are removed, and packages/spec/src/system/migration.zod.ts is a *.zod.ts under packages/spec/src/** (a content-independent limb). A clause-② card builds at the default judgment tier and is reviewed at CONTRACT_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 grep at merge base 44c917a47 vs branch head), excluding CHANGELOGs and release notes:

# Artefact Before After
1 migrateSysNotificationToEvent (the runner) 42 occurrences in 9 files; migrate-sys-notification-to-event.ts present 13 occurrences in 8 files, all of them tombstones, prose or the two new retirement pins; the module is gone from disk
2 its barrel export in packages/metadata/src/migrations/index.ts 1 export block naming it plus its 3 types 0 — replaced by a ⚰️ TOMBSTONE block in the house style of the addSysMetadataOverlayIndex tombstone above it
3 its tests migrate-sys-notification-to-event.test.ts (754 lines) and notification-migration-audit-preservation.integration.test.ts (282 lines) present; 5 references in real-driver-exec-surface.test.ts both files deleted; 0 references in real-driver-exec-surface.test.ts (its two runner cases and the now-unused engine double went with them)
4 the sys_migration receipt row for this id notification-event-migration-ledger.pin.test.ts (244 lines) pinning the ruled matrix; 7 DATA_MIGRATION_FLAG_OBJECT sites in the runner pin file deleted; runner deleted. #15710's matrix keeps the other two ids, untouched
5 the spec docblock stating the matrix 5 ledger-column bullets on NOTIFICATION_EVENT_MIGRATION_ID 0 bullets — the docblock is now a retirement tombstone naming both refused alternatives and the reversal path
6 the id's membership in CREATION_ATTESTED_MIGRATION_IDS a 3-tuple ending in NOTIFICATION_EVENT_MIGRATION_ID a 2-tuple: FILE_REFERENCES_MIGRATION_ID, VALUE_SHAPES_MIGRATION_ID

Rename 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_ID itself 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 a sys_migration row 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:

  • Production callers: 0. Excluding CHANGELOGs, docs/ and *.test.ts, every occurrence was the definition, its own usage docblock, the barrel line, one comment in driver-exec.ts and 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).
  • Instrument live: the symbol was in 17 files — up from the card's 13, because 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), plus toHaveLength(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 real attestFreshDatastore and 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_at set) 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.sh header). 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 tombstone stays 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, blob e997213… to 75c399c…; B: barrel export blocks 0 -> 1, blob 5e42483… to 23da44c…, and the restored runner byte-identical to the merge-base blob dc21ee6…. Each restore proved by git checkout HEAD -- path (⛔ never a bare git checkout --, which takes the index), a blob equal to HEAD's, and an empty git diff HEAD; the script ends by observing git status --porcelain is empty. An empty hash is treated as a FAILURE, never as "nothing to compare". The whole thing runs under trap … EXIT INT TERM with 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 to src/, never through a package exports entry to dist/, so there is nothing for ablation-dist-preflight.mjs to assert and no rebuild leg. The platform-objects suite does reach spec through exports and is deliberately not the suite ablated, for exactly that reason.

⑤ Single-writer holders, from the OPEN PR LIST

Measured from GET /pulls?state=open and each PR's own /files (its head against its own merge base) — ⛔ never from remote branches. My authored face is the 16 paths of git 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.


Coordination with #16100 — verified rather than assumed

The ruling predicted #16100 was "in flight". It is not: #16100 closed completed on 2026-09-06, two days before the ruling. What it actually shipped is the receipt writer inside the runner plus the #15710 ledger 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 in migrate-sys-notification-to-event.ts itself. 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 migrate sub-command shaped exactly like files-to-references / value-shapes — dry-run default, --apply gate, 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.

⚠️ The ruling's own confidence gap, restated rather than papered over

Nobody has measured whether any live deployment — cloud included — carries pre-ADR-0030 sys_notification rows. 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/objectstack on the final tree (⛔ never a hand-written list), then reconciled:

Run reconciliation — 88 derived, 88 run, 0 NOT-MEASURED, 0 UNRUN.
✓ dispatch-gates --ran: 88 derived famil(ies) accounted for — 88 run, 0 NOT-MEASURED.

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-loadsPREREQUISITE 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:

Command Result
pnpm --filter @objectstack/spec test 470 passed (470) files, 13191 passed (13191)
pnpm --filter @objectstack/metadata test 52 passed (52) files, 776 passed (776)
pnpm --filter @objectstack/platform-objects test 38 passed (38) files, 550 passed (550)
pnpm --filter @objectstack/runtime test 248 passed (248) files, 3511 passed (3511)
typecheck (spec, metadata, platform-objects, runtime) all green; each package's test layer compiles under its own tsconfig.test.json
pnpm --filter @objectstack/spec check:generated ✓ All 15 generated artifacts are up to date — including check:api-surface, check:authorable-surface, check:docs, check:export-origins

scripts/engine-double-contract.pinned.json was regenerated with node 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 from real-driver-exec-surface.test.ts whose engine double is genuinely gone (that file no longer needs an engine at all). Gate green after.

pnpm check:nul-bytes green, plus a hand sweep of the diff with grep -naP '[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]'.



Docs-drift round — discharged

Nothing in content/docs is falsified by this PR, and nothing there is edited. This PR touches zero content/docs paths. 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 c6f329fdfc is not fetchable here, so the comparison is anchored on its base commit fb7d75fca1, whose content/docs tree is f5fe6bec53…identical to origin/main's. My head's content/docs tree is 150e82d6e9…. The two differ in exactly one file, content/docs/permissions/system-context.mdx, and it is main's: the commit that changed it is fb7d75fca1 itself (the bot's own base), which git merge-base --is-ancestor answers 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/docs returns 0 paths.

Re-derived with node scripts/docs-audit/affected-docs.mjs --json fb7d75fca1c45dc9bfa5875df8eec6b385a2efcb: same 13 docs, and "dirty": false on this checkout — this re-derivation has no uncommitted-changes hole.

The release-owned rows — ⛔ read only, ⛔ not edited

content/docs/releases/v17/17-4.mdx is NOT falsified. Two rows name the surface (plus a third the round did not list):

  • :376 — "a7da4deadr-0030-notification-event joins CREATION_ATTESTED_MIGRATION_IDS …"
  • :226 — "8647c87 — A completed run of the ADR-0030 notification cut-over now records itself in the sys_migration deployment ledger …"
  • :382 — "c2a336c@objectstack/spec/system now 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.0 is published and is npm's latest tag; the repo is at 17.4.0 and packages/spec/CHANGELOG.md carries its ## 17.4.0 section. This PR's changeset is minor, 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. :382 is doubly safe: NOTIFICATION_EVENT_MIGRATION_ID is kept, so @objectstack/spec/system still names the cut-over today. Retro-editing a shipped release page is precisely what the content/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:384sys_notification.action_url cited as evidence for a flow-node key rename. It describes the object's column, which exists; anchored via the sys_notification string literal that the deleted runner's EVENT_OBJECT const held.
  • 17-0.mdx:756 and 17-2.mdx:240/:375meta.getLegalNextStates, reached through the SDK route bridge. Zero relation to the migration.
  • 17-1.mdx:172 — localized templates reaching sys_inbox_message: the live pipeline, which this PR does not touch.

The nine hand-written rows — judged, and most are wrong rows

Page Anchor Verdict
permissions/authorization.mdx, permissions/explain.mdx, permissions/rls.mdx not_applicable ⚠️ Wrong rows, all three. Each uses not_applicable as an authorization layer verdict in the closed vocabulary grants / 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.mdx getObject ⚠️ Wrong row, and the mechanism is nameable. The anchor is LEDGER_METHODS = ['getObject', 'find', 'insert', 'update'] and a method of interface MigrationLedger — both private, non-exported declarations that lived inside the deleted runner as a duck-typing probe. The page documents IMetadataService.getObject(name), a different declaration in a different package, untouched.
protocol/objectql/state-machine.mdx route bridged from getObject ⚠️ Wrong row, same private-name collision, one bridge further out. The page documents GET /api/v1/meta/object/:name/state/:field.
protocol/kernel/realtime-protocol.mdx recipient_id from LEGACY_COLUMNS Not falsified. Read properly: the hit is ### Case 3: Live Notifications, an illustrative WebSocket example subscribing to a generic notification object. The page never mentions sys_notification or any migration (grep: zero hits). LEGACY_COLUMNS was a private const naming the pre-ADR-0030 columns the runner cleared; deleting it changes no object shape and no column.
automation/hook-bodies.mdx sys_notification Not falsified — a code example inserting into sys_notification, an object that still exists.
concepts/north-star.mdx sys_notification_receipt Not falsified — a Drift entry: "The notification framework pipeline is ahead of the objectui bell cut-over; read-state should move through sys_notification_receipt." About the live model; still true.
api/client-sdk.mdx sys_inbox_message / sys_notification_receipt Not falsified — an ADR-0030 note that the framework materializes through sys_inbox_message and tracks read-state in sys_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.ts and packages/platform-objects/src/system/migration-flag.ts yielded no anchor. Searched every exported name of each across content/docs and docs:

  • driver-exec.tsdriverExecRefusal, 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.tsattestFreshDatastore, readDataMigrationFlag, isDataMigrationVerified, recordDataMigrationRun, CREATION_ATTESTATION_DETAIL, MigrationFlagEngine, plus the prose terms creation-attested and created-empty: two hits, both in docs/adr/0104-… — a file#symbol pointer to migration-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 names adr-0030-notification-event (grep for both: zero). Not falsified.

Positive control for the hand search: sys_migration returns 8 pages, so the search reaches the docs tree. Of those, deployment/seed-tenancy-repair.mdx:126 is the seed-tenancy repair's own row (a different id, untouched) and references/system/migration.mdx is AUTO-GEN, current — check:docs is green inside check: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/docs and docs for 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-… and docs/adr/0030-…). Three hits outside those:

  • references/automation/schemaless-node-config.mdx:159 — about script.config.template and sys_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.mda 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_notification rename/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.

⚠️ The round caught a real gap in this PR — fixed

docs/handoff/adr-0030-notification-convergence.md was 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:

  • the ### 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 breaking-change callout instructing a reader to "Do the objectui cut-over and the data migration together" — now says there is no data migration to pair it with, and that the bell starts from what the new pipeline emits;
  • the Tests line counting "the migration" among the suites that are green — the migration's own suites went with the runner.

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.mjs and pnpm check:nul-bytes all exit 0 after the change.

scripts/engine-double-contract.pinned.json −30 — the polarity, stated

⚠️ This ledger is grow-only COVERAGE, not a shrink-only ratchet — its own $comment says so: "This is the OPPOSITE polarity to engine-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 --write and 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 deleted migrate-sys-notification-to-event.test.ts; three leave because real-driver-exec-surface.test.ts no longer declares an engine double at all — its recordingEngine() existed solely for the one helper that needed an IDataEngine, 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 at pinnedRows 774 against a floor of 600, 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."

验收备注

Open question for the review tier

Should NOTIFICATION_EVENT_MIGRATION_ID itself 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 the sys_migration rows 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 the it('the id itself is KEPT …') case) if the tier reads the ruling the other way.


Generated by Claude Code

…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
@github-actions

github-actions Bot commented Sep 9, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 3 package(s): @objectstack/metadata, @objectstack/platform-objects, @objectstack/spec, touching 40 documentable anchor(s). ⚠️ 2 changed file(s) yielded no anchor (packages/metadata/src/migrations/driver-exec.ts, packages/platform-objects/src/system/migration-flag.ts), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

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

  • content/docs/api/client-sdk.mdx (via sys_inbox_message (literal, a string literal in INBOX_OBJECT), sys_notification_receipt (literal, a string literal in RECEIPT_OBJECT))
  • content/docs/automation/hook-bodies.mdx (via sys_notification (literal, a string literal in EVENT_OBJECT))
  • content/docs/concepts/north-star.mdx (via sys_notification_receipt (literal, a string literal in RECEIPT_OBJECT))
  • 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), not_applicable (literal, a string literal in SysNotificationMigrationResult; a string literal in runNotificationEventMigration))
  • content/docs/permissions/explain.mdx (via not_applicable (symbol, a field of const object LEDGER_CLAIM), not_applicable (literal, a string literal in SysNotificationMigrationResult; a string literal in runNotificationEventMigration))
  • content/docs/permissions/rls.mdx (via not_applicable (symbol, a field of const object LEDGER_CLAIM), not_applicable (literal, a string literal in SysNotificationMigrationResult; a string literal in runNotificationEventMigration))
  • content/docs/protocol/kernel/realtime-protocol.mdx (via recipient_id (literal, a string literal in LEGACY_COLUMNS; a string literal in runNotificationEventMigration))
  • content/docs/protocol/objectql/state-machine.mdx (via /object/:name/state/:field (route, bridged from symbol getObject — its route source's handler names it))

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

  • content/docs/releases/v17/17-0.mdx (via sys_notification (literal, a string literal in EVENT_OBJECT), 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/v17/17-1.mdx (via sys_inbox_message (literal, a string literal in INBOX_OBJECT))
  • content/docs/releases/v17/17-2.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/v17/17-4.mdx (via CREATION_ATTESTED_MIGRATION_IDS (symbol, a top-level const object))

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/src/migrations/driver-exec.ts, packages/platform-objects/src/system/migration-flag.ts) — pages documenting those are invisible to this run
  • 10 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 60 of 215 client-bound route-ledger rows — the other 155 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 155: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 100 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 — 137 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 0780e88481c302870b151aa40ef8d20935032d46packageMentionDocs.

Which tree this was computed on

This run read content/docs from f9302b332f24409eeb37e9b368994792a835db09 — the merge of head 28eb180fdfe48d9333a3a1400c1e81da34b3384f into base 0780e88481c302870b151aa40ef8d20935032d46, 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 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

⚠️ 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 0780e88481c302870b151aa40ef8d20935032d46 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

…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

Copy link
Copy Markdown
Collaborator

Contract review at CONTRACT_REVIEW_TIERVerdict: PASS WITH FINDINGS (audit reading; director seat, summon #18 segment 6, session_017Js5kTpTtxieBjPyScgxJ3, 2026-09-10T23xxZ)

PR #17194 · head 12bc827ac47487b035c04248e884a476e98f0004 (re-read at posting 23:58:35Z; unchanged since 15:57Z) · card #16194.


Verdict: PASS WITH FINDINGS

No blocking finding. The diff executes decision batch #88 exactly: six named artefacts removed, both refused alternatives absent, nothing widened.

Head reviewed

12bc827ac47487b035c04248e884a476e98f0004 (matches prefix 12bc827ac4; head did not move). Merge base with origin/main (ae19f5edb): fd5cff209f. 16 files, +525/−2126; PR is draft, auto_merge: null.

Clause-② reading

  • My reading: yes, on both limbs. Path limb: packages/spec/src/system/migration.zod.ts is a *.zod.ts under packages/spec/src/** (SKILL.md:641, content-independent). Declaration limb: the published tuple CREATION_ATTESTED_MIGRATION_IDS narrows 3→2 (migration.zod.ts:228-231, type (typeof …)[number] loses an arm) and four exported symbols leave the published entry @objectstack/metadata/migrations (index.ts:49-54 on main → tombstone :49-77). Strictly the SKILL criterion reads "widen/enlarge" and this PR shrinks, but the path limb fires regardless and "unsure ⇒ yes" applies. No Contract-text: needed.
  • Claim match: newest Claim: (card comment 5602702791) says Clause-②: yes; PR body says Clause-②: yes. Match.
  • check-clause2-carriers.mjs --pair 17194: exit 0 ("declaration readable in the fixed spelling, both carriers agree"). needs:contract-review present on both card and PR.

Governed surface / protocol subtree

  • Governed register (docs/adr/** · .claude/** · skills/** · AGENTS.md · CLAUDE.md): none of the 16 paths. docs/handoff/adr-0030-notification-convergence.md is outside the register (hand-merge not required).
  • Protocol subtree touched: packages/spec/src/system/** only (migration.zod.ts + new notification-event-migration-retirement.test.ts; deleted notification-event-migration-ledger.pin.test.ts) ⇒ protocol:system is the correct label; no data/ui/ai.

CI on head

40 check-runs on 12bc827ac4: 35 success, 5 skipped, 0 red. Required floor: Lint & Repo Gates success (102540692859), TypeScript Type Check success (102546608034); Check Changeset, Governed Surface Queue Guard, Spec property liveness, single-writer/issue-claim guards all success. Skipped: Auto Label/Check PR Size (duplicate triggers), Build Docs, Packed-tarball smoke (opt-in), Console Pin Gate — checked by hand: objectui at the pinned 53ded82bf7 (.objectui-sha at PR head) has zero references to migrateSysNotificationToEvent|SysNotificationMigration|CREATION_ATTESTED_MIGRATION_IDS|NOTIFICATION_EVENT_MIGRATION_ID|adr-0030-notification-event; positive control: objectui imports @objectstack/metadata (packages/data-objectstack/src/index.ts) and @objectstack/spec in 468 files. The removal cannot break the pinned sibling.

Local gates on the ref: check-changeset-no-major.mjs --base origin/main --head refs/pm-review/17194 exit 0 (no major; level axis N/A offline — CI's PR-scoped Check Changeset is the reading, success). check-adr-0087-registration.mjs same refs exit 0: one declared-breaking changeset carrying not-required (no-migration-prescription) on a positive reading (no FROM→TO prescription in the body).

Derived judgments (published-face delta vs the ruling)

# 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

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


Generated by Claude Code

Copy link
Copy Markdown
Collaborator

Landing provenance — director seat, summon #18 segment 6 (session_017Js5kTpTtxieBjPyScgxJ3, 2026-09-10T00:13:46Z). Clearing needs:contract-review on both carriers (#17194 + card #16194) on the strength of the contract-review-tier PASS WITH FINDINGS at #17194 (comment) (head 12bc827ac4, unchanged). Landing pre-checks (contract-review.md ①②③): ① tier verdict on the card (pointer posted); ② check-clause2-carriers.mjs --pair 17194 exit 0; ③ 34 check-runs on head, 0 red / 0 in progress; governed-surface test exit 0 (not governed); mergeable_state: clean. Next: ready → auto-merge (SQUASH) → merge-queue entry, per landing-operations.md. Executed under the maintainer's 2026-09-09 13:4xZ order 「把当前的契约复审全部处理完」 precedent; the dispatching seat keeps ACCEPT/landing-window duties (MERGED confirmation + card close-out) if it is back before the queue finishes — otherwise this seat closes out. ⚠️ Auto-merge is not armed yet: this PR takes a modify/delete conflict with #17073 (both reviewed PASS today); #17073 enters the queue first, then this PR's mergeable_state is re-read — clean ⇒ enqueue, dirty ⇒ a patch round merges origin/main keeping the deletion.


Generated by Claude Code

@huangyiirene
huangyiirene marked this pull request as ready for review September 10, 2026 00:14

Copy link
Copy Markdown
Collaborator

Landing window — mergeable_state re-read: dirty. A patch round is owed before this can be armed.

The conflict the landing plan at 5610610263 sequenced for has materialised. #17073 merged at 2026-09-10T00:41:28Z, carrying its 3-line operatorFacingErrorText(err) hunk inside packages/metadata/src/migrations/migrate-sys-notification-to-event.ts — the file this PR deletes. Re-read just now: mergeable_state: **dirty**, head unchanged at 12bc827ac4, draft: false, 44 check runs with 0 red.

⇒ The dirty branch of that plan applies: a patch round merges origin/main into claude/issue-16194-retire-notification-event-migration keeping the deletion (the batch #88 ruling retires the runner, so #17073's hunk goes with the file), then mergeable_state is re-read and the PR is armed.

Not armed, and not arming until that merge lands — arming a dirty PR puts a conflict into the queue.

Who takes it. The dispatching seat (domain:cli execution PM, seat post #6024) is back inside the landing window and holds the ACCEPT/close-out duties the director seat handed back. The patch round itself is a dev act — a worktree merge and a push — ⛔ not a PM write, so it is dispatched rather than done here. This seat's dispatch concurrency is held at 3 by a standing maintainer instruction with three deliveries in flight; the patch round goes out as the next slot frees, and this PR is not left unattended in the meantime.

My reading, offered rather than decreed: the CONTRACT_REVIEW_TIER PASS WITH FINDINGS at 5610459957 stands through the patch round, because a base merge adds no authored change to the 16-path diff it was taken on. If the tier reads a moved head as owing a re-read, say so and it will be re-run before arming.

⚠️ One stale line, recorded rather than edited. This PR body's own banner still reads 「⛔ do not flip it ready, ⛔ do not arm auto-merge, ⛔ do not work around the label」. That was a true instruction while CONTRACT_REVIEW_TIER was measured unavailable to the dispatching session — it was answered by the PASS at 23:58Z and the label clearance at 00:13Z. It is frozen prose describing a hold that has been lifted, not a live instruction; the body is left as the dev wrote it.


Generated by Claude Code

…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>
@os-project-manager
os-project-manager added this pull request to the merge queue Sep 10, 2026

Copy link
Copy Markdown
Collaborator

Patch round landed, CI green, conflict gone — and one thing the tier should read before this merges

Dispatching seat (domain:cli execution PM, seat post #6024). New head 28eb180fdfe48d9333a3a1400c1e81da34b3384f — a real merge commit, parents 12bc827ac4 (branch tip) + 5a95b0e93d (origin/main). Fast-forward push; ⛔ no rebase, no amend of pushed history, no force.

mergeable_state: dirtyblockedclean. CI on the new head: 34 check runs, 0 red, 0 in progressLint & Repo Gates, TypeScript Type Check, Test Core rollup, Dogfood Regression Gate, Build Core, Temporal Conformance, Spec property liveness, Governed Surface Queue Guard, Check Changeset all success.

⚠️ This was NOT a pure base merge, and that is the thing to read

My earlier note (5610922756) said the CONTRACT_REVIEW_TIER PASS WITH FINDINGS at 5610459957 should stand through the patch round "because a base merge adds no authored change". That description no longer fits what happened, so it is corrected here rather than left to stand.

Conflict 1 — mechanical. packages/metadata/src/migrations/migrate-sys-notification-to-event.ts, DU. Resolved with git rm: the batch #88 ruling retires the runner, so #17073's three-line operatorFacingErrorText(err) hunk goes with the file it edits. Confirmed D in git diff --name-status origin/main...HEAD.

⭐ Conflict 2 — git raised nothing, and merging clean would have shipped an unbuildable branch. #17073 also added packages/metadata/src/migrations/raw-exec-operator-detail-16657.test.ts. Its line 44 is import { migrateSysNotificationToEvent } from './migrate-sys-notification-to-event.js'; and its lines 165–193 are a describe block testing that runner. Git was silent because this branch had never touched that file.

The delivering seat removed that import line and that describe block. Deletion only — 193 → 162 lines, and 0 added lines on that file's diff against origin/main. The three sibling describe blocks (migrateEnvIdToProjectId, migrateProjectIdToEnvironmentId, dropProjectionTables) are untouched, as is every other file main brings.

The direction is close to forced: this branch's own pin notification-event-migration-retirement.test.ts asserts that no file in this package imports the module under any spelling, so leaving main's block red-lines the retirement's own guard — and the module no longer exists, so the block would not compile either way. The ruling also names "its tests" among what is removed in one PR. It is still a call rather than a mechanical resolution, which is why the seat stated it in its report and in the merge commit body, and why it is restated here.

My reading, offered rather than decreed: those 31 deleted lines move no contract face — no export, no schema, no payload, no accept set — so the PASS's own derivation (the published tuple narrowing 3→2, four symbols leaving @objectstack/metadata/migrations) is untouched, and the verdict's subject is unchanged. ⚠️ But it is a deletion the tier did not see, on a file the tier did not read, so if the tier reads a moved head as owing a re-read, say so and this will not enqueue until it has one.

Check readings — every count moved up

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

Copy link
Copy Markdown
Collaborator

Enqueue confirmed — closing the open question in the comment above. pull_request.enqueued arrived at 02:17:37Z, 14 seconds after the arming call. So auto-merge did take; the empty merge-method and timestamp in that API response were a degenerate response shape, not a refusal, and the clean state made the queue accept it far faster than the ~55–66s the three PRs armed earlier tonight took from a blocked state.

⇒ Read the previous comment's arming caveat as resolved: #17194 is in the merge queue. Everything else in it stands — in particular the ⚠️ that the patch round was not a pure base merge, and that the 31-line deletion in raw-exec-operator-detail-16657.test.ts is a call the tier did not see on a file the tier did not read. If the tier wants a re-read before this lands, it is still worth saying now.


Generated by Claude Code

Copy link
Copy Markdown
Collaborator

⚠️ mergeable_state: dirty since #17073 merged (00:41Z) — modify/delete conflict on migrate-sys-notification-to-event.ts. Patch-round handoff to the claiming seat is on card #16194 (director seat, summon #18 segment 6). Auto-merge not armed until the branch carries origin/main.


Generated by Claude Code

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 protocol:system size/xl tests tooling

Projects

None yet

3 participants