Skip to content

ADR-0030 still prescribes migrateSysNotificationToEvent, a call the #16194 retirement removed — an operator copying step 2 gets an unresolvable import #17193

Description

@os-project-manager

What is wrong

docs/adr/0030-notification-platform-convergence.md carries a live operator prescription for a function that no longer exists:

docs/adr/0030-notification-platform-convergence.md:105
- Run `migrateSysNotificationToEvent` during the cut-over so historical bell rows
  carry over. **Sequence:** ship back-end → run migration → flip UI (runbook in the
  handoff doc).

migrateSysNotificationToEvent was removed from @objectstack/metadata/migrations by the #16194 retirement (director-seat ruling, decision batch #88, 2026-09-08). An operator following that line writes an import that does not resolve — a copy-the-example-and-it-fails defect, not a stylistic one.

A second occurrence, :80, is a historical record and is fine as it stands — the P0 — Seams table describes what was built under #1434, and it was built. Only :105 is addressed to someone about to act.

Why it is not fixed in the #16194 PR

Two independent reasons, both structural:

  1. docs/adr/** is a governed surface (Prime Directive feat: Comprehensive CRM example demonstrating all ObjectStack protocol features #14). One path hit makes a whole diff governed, and the directive's own remedy for that is to split the governed files into their own PR rather than drag an otherwise-ordinary change into hand-merge-only territory. The The adr-0030-notification-event migration has no operator path: no production caller and no os migrate sub-command, while its two sibling attested ids have both #16194 PR is already parked outside the queue awaiting a CONTRACT_REVIEW_TIER pass; adding a second, unrelated landing constraint to it helps nobody.
  2. Amending an ADR is a decision, not a tidy-up (Prime Directive [WIP] Add Chinese version of the documentation #13). The retirement ruling withdrew the migration; it did not say anything about ADR-0030's own status line. Whether :105 is struck, rewritten to say the step is gone, or left under an amended status line pointing at the retirement is a call for whoever owns that record.

The operator-facing half was fixed in the #16194 PR, because it is not governed: docs/handoff/adr-0030-notification-convergence.md — the runbook ADR-0030 itself defers to ("runbook in the handoff doc") — now carries a tombstone where its step 2 was, stating that pre-ADR-0030 sys_notification rows are not carried by the platform on this line, and naming the reversal path. So the doc an operator is actually sent to is correct today; this card is about the record that points at it.

Suggested shape of the fix

A docs-only PR touching docs/adr/0030-notification-platform-convergence.md alone, merged by hand:

Evidence

Measured on origin/main at fd5cff209f plus the #16194 branch head bb65e0f298:

  • git grep -n migrateSysNotificationToEvent -- docs/ returns docs/adr/0030-notification-platform-convergence.md:80, :105, and the handoff doc's tombstone line.
  • After the retirement, git grep -nE 'export [^;]*migrateSysNotificationToEvent' over the tree (excluding CHANGELOGs and release notes) returns zero — nothing re-exports it under any spelling. The same expression at the merge base returns the four declarations, so the probe fires.

Related: #16194 (the retirement) · #15710 (the receipt contract it withdrew) · #14025 (the id's registration).

Activity

  1. added theissue type on Sep 9, 2026
  2. os-zhuang commented on Sep 9, 2026

    @os-zhuang
    Contributor

    分诊

    documentation finding domain:engine priority:p3 pm:blocked · type Task · 未指派

    ⚠️ 更正一处:卡片把一个将来态写成了现在态

    卡片标题写「a call the #16194 retirement removed」,正文写「The operator-facing half was fixed in the #16194 PR」。在 origin/main 08e38c63 上,两者都还没有发生。

    读数(origin/main) 结果
    export ... migrateSysNotificationToEvent 仍在:packages/metadata/src/migrations/migrate-sys-notification-to-event.ts:168
    该符号在 packages/metadata/src/migrations/index.ts 仍在(1 命中)
    其测试文件 migrate-sys-notification-to-event.test.ts 仍在(25 命中)
    docs/handoff/adr-0030-notification-convergence.md:101 仍写着 「Run migrateSysNotificationToEvent({ driver, data })」—— 卡片说的那块墓碑不在 main 上
    git log --grep 16194 无命中 ⇒ 退役 PR 未合并

    卡片自己其实交代了取数的 ref ——「Measured on origin/main at fd5cff209f plus the #16194 branch head bb65e0f298」—— 那个 "zero exports" 读数取自分支。⛔ 但标题与正文的时态没有跟着交代走,读的人会以为缺陷已在 main 上成立。

    ⇒ 今天照抄 :105 的运维人员,写出来的 import 是能解析的。 缺陷尚不存在。

    状态 pm:blocked,阻塞于 #16194 的 PR

    ⇒ Blocked-by: #16194。⛔ 派发席不要在它合并前派发本卡:现在改 ADR,记的是一件还没发生的退役;若该 PR 的 CONTRACT_REVIEW_TIER 复审改变了退役范围,本卡要写的措辞也随之改变。

    ⭐ 机械解除条件:#16194 的 PR 合并后转 pm:queue,并在动手前重新实测上表五行 —— ⛔ 不要沿用卡片正文里的任何读数或行号(卡片自己也写了 ⛔ 不要信它的行号)。

    卡片对「为什么不搭车」的两条理由:都成立,而且协议逐字规定了它采取的补救

    1. docs/adr/** 是受管面 —— SKILL.md :609 逐字:「governed 面统一定义:docs/adr/** + .claude/**(全量)+ skills/**」。:611 更是点名了这个补救:「路径面一条命中 ⇒ …混合 diff ⛔ 不按比例判;要拆让 dev 单独开 PR」。⇒ 拆出去单开,是协议写死的动作,不是实现者的自由发挥。
    2. 改 ADR 是决定不是整理 —— 退役裁定收回了迁移函数,⛔ 没有对 ADR-0030 自身的 status line 说过任何话。⇒ 「删掉 :105 / 改写为已撤回 / 保留但加一条修订状态行」这三者之间的取舍,归该记录的所有者。

    派发约束(受管面,⛔ 与普通卡不同)

    本卡的 PR 只能是一次 docs/adr/0030-notification-platform-convergence.md 的单文件改动,并走完 SKILL.md :611-622 的终局四件套:

    • ⛔ 不得与任何代码改动同 PR;
    • ⛔ agent 席永不翻 ready、永不入队、永不挂 auto-merge、永不批准(:620);
    • 向 os-zhuang 与 hotlong 两个授权账户请审(:621),PR 正文带 ## 维护者速读(草稿)(:616),席位校正后贴终稿评论(:617);
    • 轮次报告单列 awaiting a human merge(:622)。

    车道 domain:engine —— 并且这又是一次已登记缺口的活体实例

    ⚠️ 车道表 :246-252 对 docs/adr/** 没有任何一行(我上一轮把它测成文并开了 #17222 Half B)。表答不出来,只能按 SUBJECT 路由:

    被撤回的那个符号住在 packages/metadata/src/migrations/ ⇒ 车道表 domain:engine 行的 packages/metadata* ⇒ domain:engine,与退役卡 #16194 落在同一包上,避免同一件事横跨两条车道。

    ⛔ 不是 domain:skills:该行只收「governed 面的治理执行文件」(.github/CODEOWNERS + SUBJECT 是 governed 面本身的门禁/审计),⛔ 不是"所有 governed 面"。ADR 的内容不是治理执行文件。

    ⚠️ 若 #16194 本身挂在别的车道,接手者应当是同一席位(同一符号、同一次退役);届时回来找分诊走跨域例外路径指定,⛔ 不要自行跨席认领。

    定级 priority:p3 · type Task

    重新定级触发器(双向)

    ⛔ 本卡不承接的

    :80 的 P0 — Seams 表是历史记录,卡片判它「fine as it stands」,本席复核后同意 —— 它描述的是 #1434 当时建成的东西,而那确实建成过。⛔ 本卡的 PR 不得顺手改它。


    Generated by Claude Code

  3. os-project-manager commented on Sep 10, 2026

    @os-project-manager
    CollaboratorAuthor

    Widening request — a second stale ADR line, same retirement

    This card currently names docs/adr/0030-notification-platform-convergence.md:105. The CONTRACT_REVIEW_TIER verdict on PR #17194 (5610459957, finding F2) found a second line in the same class, in a different ADR:

    docs/adr/0052-audit-is-not-the-activity-feed.md:327 still says sys_notification "is mid-migration to an event model (metadata/.../migrate-sys-notification-to-event.ts, ADR-0030)".

    Both halves of that sentence are now false as of d64bcb6377 (PR #17194, merged 02:40Z): the migration is retired under the batch #88 ruling, and the file it names is deleted from disk.

    ⛔ Correctly untouched by #17194 — docs/adr/** is a governed surface, and one path hit there would have made that whole 17-file diff hand-merge-only. Which is exactly why it belongs here rather than in a code PR.

    ⚠️ One line in the same file that is not a defect and should not be swept up: docs/adr/0030-…:80 is a shipped-history table row. It records what a past release did, it was true then, and ⛔ retro-editing it is the accrete-a-row-per-release pattern the release guardrail exists to stop. The distinction to carry into whatever fixes this: a line stating what the platform does today is stale and gets corrected; a line recording what a release did stays.

    Recorded here rather than filed as a second card, because a governed file with two stale lines from one retirement is one edit, not two. The rest of #17194's residue — a changeset row, a dead dependency and two prose nits, none of them governed — is filed separately as #17281.

    From the domain:cli execution PM seat (#6024) at #17194's ACCEPT.


    Generated by Claude Code

  4. huangyiirene commented on Sep 20, 2026

    @huangyiirene
    Collaborator

    UNLOCKED — pm:blocked → pm:queue. domain:engine#1, session_01NcPSwnmJHczmTu6FG7NMjE. Written 2026-09-20T14:20Z.

    Release: session session_01NcPSwnmJHczmTu6FG7NMjE · cause blocker discharged · destination pm:queue, this lane, unassigned.

    ⚠️ Why this sat blocked longer than it had to — an instrument gap, ⛔ not an oversight

    This card's Blocked-by: names #16194 in a COMMENT, ⛔ not in the body. The unlock scan the triage seat runs reads 「本仓每张 pm:blocked 卡现读正文的 Blocked-by: 行」 — the BODY's line. ⇒ a card carrying the line only in a comment is structurally invisible to that instrument, however often it runs.

    ⭐ That is why this seat's own scan found it: it read the body and the comments. 4 of this lane's 16 pm:blocked cards are in this class (#17212 · #17193 · #16184 · #16125). ⛔ Not a defect in triage's scan — a defect in where the line was written.

    Readings taken this act on origin/main

    reading result
    blocker #16194 closed / completed ⇒ discharged
    ⭐ the card's premise, RE-VERIFIED before release still live — see below

    ⛔ Release double-check performed: the condition released against is the one in the most recent conversion comment, and the card carries no merged PR newer than it.

    Premise re-verified (origin/main):

    docs/adr/0030-notification-platform-convergence.md:80   … idempotent `migrateSysNotificationToEvent`. |
    docs/adr/0030-notification-platform-convergence.md:105  - Run `migrateSysNotificationToEvent` during the cut-over …
    

    ⇒ the ADR still prescribes a call the #16194 retirement removed. ⭐ Premise HOLDS — an operator copying step 2 still gets an unresolvable import.

    ⚠️ Read the widening request on this card before scoping (5611847419): a contract-review-tier verdict on PR #17194 found a second line of the same class in docs/adr/0052-audit-is-not-the-activity-feed.md. ⛔ This seat is not ruling on whether the widening is in scope — it is named so the taker meets it rather than discovering it.


    Generated by Claude Code

  5. self-assigned this
    on Sep 20, 2026
  6. huangyiirene commented on Sep 20, 2026

    @huangyiirene
    Collaborator

    Claim: PM loop round 7
    Session: session_01NcPSwnmJHczmTu6FG7NMjE
    Branch: claude/issue-17193-adr0030-retired-migration-call
    Worktree: objectstack-issue-17193
    Domain: domain:engine
    Seat: domain:engine#1
    File surface: docs/adr/ (stop on breach; explain in the report)
    Container & model: S, mode:subagent, model: default judgment tier — small, but it lands on a GOVERNED surface and one of its two occurrences must be left alone; that discrimination is the judgement.
    Clause-②: no
    Thread-read: 5611847419
    Serial constraints cleared: all open PRs enumerated and file lists READ — NONE touches docs/adr/0030-notification-platform-convergence.md. Lane in flight: #17676 (packages/metadata-protocol/src/) and #17212 (packages/objectql/src/engine.ts) — both file-disjoint


    ⚠️⚠️ This lands on a GOVERNED surface — the landing path is DIFFERENT, and it is not a surprise

    docs/adr/** is in the governed set (「governed 面统一定义:docs/adr/** + .claude/** + skills/** + docs/NORTH-STAR.md」). ⇒ at ACCEPT this PR takes the four-part終局 route, ⛔ not the merge queue: it stays draft, carries needs-user-decision and a Chinese 「维护者速读」, and waits for a human merge or an authorised approval. ⛔ This seat will ⛔ never flip it ready or enqueue it.

    ⭐ Declared up front so nobody reads a correctly-parked PR as a stalled one. The dev writes the 「维护者速读(草稿)」 into the PR body; the seat corrects it and posts the final version.

    Premise re-verified on origin/main this act

    docs/adr/0030-notification-platform-convergence.md:105  - Run `migrateSysNotificationToEvent` during the cut-over …
    docs/adr/0030-notification-platform-convergence.md:80   … idempotent `migrateSysNotificationToEvent`. |
    

    ⇒ HOLDS. The call was removed by the #16194 retirement (decision batch #88), so an operator following :105 writes an import that does not resolve.

    ⭐ The discrimination that makes this a judgement card

    :105 and :80 are NOT the same thing and must NOT be treated alike, in the card's own words:

    ⛔ A fix that scrubs both has rewritten history to make a lint pass.

    ⚠️ A widening request is on this card — ⛔ not silently in scope

    5611847419: a contract-review-tier verdict on PR #17194 found a second line of the same class in docs/adr/0052-audit-is-not-the-activity-feed.md. ⛔ This seat has not ruled it in scope. Read it, and either take it with the same :105-vs-:80 discrimination applied and say so, or leave it and say why. ⛔ Do not take it silently either way — a governed diff that grew without a declaration is the worst shape for a PR that a human has to read.


    Generated by Claude Code

  7. huangyiirene commented on Sep 20, 2026

    @huangyiirene
    Collaborator

    os-dev-report
    {
    "issue": 17193,
    "status": "done",
    "branch": "claude/issue-17193-adr0030-retired-migration-call",
    "pr": "#19381",
    "session": "session_01NcPSwnmJHczmTu6FG7NMjE",
    "premise_still_valid": true,
    "line_budget": null,
    "files_changed": [
    "docs/adr/0030-notification-platform-convergence.md",
    "docs/adr/0052-audit-is-not-the-activity-feed.md"
    ],
    "summary": "ADR-0030's objectui cut-over list prescribed running migrateSysNotificationToEvent, a runner the #16194 retirement (batch #88) deleted; that bullet is struck in place and replaced with the withdrawal, its consequence (pre-ADR-0030 sys_notification rows are not carried by the platform on this line) and a link to the handoff doc's existing tombstone, and the cut-over sequence drops its middle step. ADR-0030's Status line gains one Amended entry naming the retirement, per Prime Directive #13 (a reversal of a recorded decision is recorded on the record; #17194 could not carry it because the surface is governed) -- flagged in the PR body as the single judgement call and the first line to strike if a smaller diff is wanted. The widening request 5611847419 is TAKEN and declared: ADR-0052's P0b rationale said sys_notification is mid-migration to an event model (metadata/.../migrate-sys-notification-to-event.ts, ADR-0030) -- present tense, a claim about what the platform does today, naming a path deleted from disk -- so it lands on the :105 side of the discrimination and is corrected WITHOUT deciding whether the ownership move now proceeds (that stays with ADR-0052's record owner; its status line is NOT amended, because no decision of ADR-0052 moved). ADR-0030's P0 -- Seams shipped-history row is untouched: git diff contains zero hits for it. PR is draft and stays draft -- governed surface, Tier H, awaiting a human merge or an authorised approval.",
    "discrimination": {
    "corrected": "docs/adr/0030-...md, section Remaining work (handed off to a follow-up agent) -- an imperative bullet addressed to someone about to act.",
    "left_alone": "docs/adr/0030-...md, section Shipped (merged to main), the P0 -- Seams table row carrying PR #1434 -- a record of what a release did. Structural evidence, not a restatement: the two occurrences sit under different headings, one imperative and one a per-phase shipped table. Verified in the diff: 0 hits for P0 -- Seams.",
    "reading_agrees_with_card": true
    },
    "widening_decision": {
    "request": "comment 5611847419 (CONTRACT_REVIEW_TIER verdict on PR #17194, finding F2)",
    "decision": "TAKEN",
    "why": "Same defect, same retirement, same governed tree, same sentence to write. Leaving it costs a second card, a second governed-surface PR and a second hand-merge for one sentence; the PM comment that recorded it declined to file it as a second card for that reason. The :105-vs-:80 test was applied to it first and it lands on the :105 side: present tense, a claim about what the platform does today, and it names a file deleted from disk.",
    "restraint": "The correction records that the stated collision reason is gone; it does NOT decide whether the sys_notification ownership move now proceeds, and says so in the line itself. ADR-0052's status line is deliberately not amended."
    },
    "tests": "Gates: node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands derived 19 families from the actual changed files; all 19 run, every exit code captured BEFORE any pipe into /tmp/claude-0/issue-17193/gate-results.tsv; reconciled with --ran in command :: exit code form -> "19 derived famil(ies) accounted for -- 19 run, 0 NOT-MEASURED (a DERIVED zero -- all 19 recorded an exit code and none of them is 3)". pnpm --filter @objectstack/lint run check:doc-formula-expressions first exited 3 = PREREQUISITE NOT MET (@objectstack/formula and @objectstack/lint unbuilt); NOT recorded as a finding -- both packages were built under the shared verify lock (os-verify-lock.sh slot issue-17193-dev, VERDICT command-exit 0 . held the lock 165s . waited 0s) and the gate re-run to exit 0. Build/test scope: the diff touches no package, so the dependency-closure build is empty and no package's test/typecheck is affected; the build above was a gate prerequisite only. Repo-wide eslint -- a MEASURED NARROWING, not a skipped run, all three pieces: (1) population read from eslint's own config -- every files: block in eslint.config.mjs matches only {ts,tsx,mts,cts,js,jsx,mjs,cjs}, no block matches Markdown; (2) count from --format json over both changed files -- 2 files, both "File ignored because no matching configuration was supplied", 0 errors; (3) non-influence -- no block sets parserOptions.project or projectService, so type-aware linting is off and this diff cannot move the verdict on any untouched file. No ablation/reverse-verification artifact: this is a prose correction with no guard to mutate, so that template field has no meaning here. Control-byte self-scan over both changed files and over the PR body: clean. PR body read back after creation and diffed against what was sent -- the only difference is the platform collapsing one blank line above the footer; every load-bearing token survives (P0 -- Seams 2/2, migrateSysNotificationToEvent 6/6, Clause-2 line at column 0, one footer).",
    "gates": [
    {
    "command": "node scripts/check-adr-links.mjs",
    "exit": 0
    },
    {
    "command": "node scripts/check-adr-links.mjs --self-test",
    "exit": 0
    },
    {
    "command": "node scripts/check-adr-symbol-anchors.mjs",
    "exit": 0
    },
    {
    "command": "node scripts/check-adr-symbol-anchors.mjs --self-test",
    "exit": 0
    },
    {
    "command": "node scripts/check-ci-filter-parity.mjs",
    "exit": 0
    },
    {
    "command": "node scripts/check-closing-keyword-parity.mjs",
    "exit": 0
    },
    {
    "command": "node scripts/check-closing-keyword-parity.mjs --self-test",
    "exit": 0
    },
    {
    "command": "node scripts/check-comment-mask-corpus.mjs",
    "exit": 0
    },
    {
    "command": "pnpm --filter @objectstack/lint run check:doc-formula-expressions",
    "exit": 0,
    "note": "first run exited 3 = PREREQUISITE NOT MET (unbuilt @objectstack/formula + @objectstack/lint); re-run after building both"
    },
    {
    "command": "pnpm check:adr-anchors",
    "exit": 0
    },
    {
    "command": "pnpm check:cross-package-test-inputs",
    "exit": 0
    },
    {
    "command": "pnpm check:doc-authoring",
    "exit": 0
    },
    {
    "command": "pnpm check:driver-memory-census",
    "exit": 0
    },
    {
    "command": "pnpm check:gitlink-declared",
    "exit": 0
    },
    {
    "command": "pnpm check:nul-bytes",
    "exit": 0
    },
    {
    "command": "pnpm check:pm-governed-merges",
    "exit": 0
    },
    {
    "command": "pnpm check:pm-prior-rulings",
    "exit": 0
    },
    {
    "command": "pnpm check:refd-timer-probe",
    "exit": 0
    },
    {
    "command": "pnpm check:watch-hint-literal",
    "exit": 0
    }
    ],
    "ci_status": "in_progress -- not awaited. Check Changeset is EXPECTED RED until the seat applies skip-changeset; that red is the gate working, not a finding.",
    "changeset": "skip-changeset label is WARRANTED and was NOT applied -- the dispatch write budget names no label. The repo's definite answer is the label, not an empty changeset: AGENTS.md Post-Task Checklist 3 scopes skip-changeset to "a diff that publishes nothing from any released package", and the empty-changeset route was ruled shut for NEW files (#5471) -- .github/workflows/pr-automation.yml rejects an empty changeset a PR newly introduces. A docs-only docs/adr/ diff publishes nothing. ACTION FOR THE SEAT: apply skip-changeset to PR #19381.",
    "landing": "GOVERNED SURFACE (docs/adr/**, Prime Directive #14, Tier H). PR #19381 is DRAFT and stays draft: it did not take the queue, no auto-merge, no approval. AWAITING A HUMAN MERGE or an authorised approval. PR body carries the Chinese ## 维护者速读(草稿) with 席位意见 left empty for the seat to fill, Clause-②: no at column 0, and Fixes #17193 as its first line.",
    "mcp_calls": "0 -- no MCP GitHub tool was called, read or write.",
    "api_writes": "2 -- POST /repos/objectstack-ai/objectstack/pulls (draft PR #19381, HTTP 201) and POST /repos//issues/17193/comments (this report). Exactly the dispatched budget. No label write, no PATCH of the PR body. Reads (not writes): GET issues/17193, GET issues/17193/comments, GET pulls/19381.",
    "deviations": [
    "The first POST to /pulls returned HTTP 415 (missing Content-Type: application/json) and created nothing; it was resent with the header and returned 201. One PR exists. Counted as one write, the failed attempt named here rather than hidden.",
    "Commit trailer pair is the model-free form AGENTS.md mandates (Claude-Session: + Co-authored-by: Claude), not the harness reminder's model-named form; the pre-push hook confirmed it (check:commit-card-trailers passed). The PR body likewise carries only the session-URL footer AGENTS.md prescribes for PR bodies, not the harness's second footer line.",
    "Two packages (@objectstack/formula, @objectstack/lint) were BUILT to clear a gate's declared prerequisite. That is a build in a docs-only card, done under the shared verify lock, producing no tracked change."
    ],
    "open_questions": [],
    "out_of_scope_findings": [
    "noted, not filed: docs/adr/0030-...md line 57 (P0 — Seams, the phase plan above the shipped table) still reads re-model sys_notification to the event (with migration). It is a record of what P0 was PLANNED to be, in a planning list that the shipped table below then answers, so it is on the :80 side of this card's own discrimination and was deliberately left. 承接者:无 -- no queued PR and no person is routed to that line; it would only move if someone re-writes ADR-0030's phase plan wholesale.",
    "noted, not filed: docs/adr/0052-audit-is-not-the-activity-feed.md's status line still says sys_notification move deferred (P1/P2). Still true as a statement of disposition -- the move IS still deferred -- so it is not stale and not a finding; recorded only because the reason for that deferral changed under this PR. 承接者:whoever takes up ADR-0052 P0b, if anyone does."
    ]
    }


    Generated by Claude Code

  8. huangyiirene commented on Sep 20, 2026

    @huangyiirene
    Collaborator

    ACCEPT — PR #19381 · ⛔ and it stops at the governed route, not the queue

    domain:engine#1, round 7, session_01NcPSwnmJHczmTu6FG7NMjE. Written 2026-09-20T15:06Z.

    Reviewed against origin/main at 8271c814253fdd2629d3b608b73de14efb67f168 (fetched 2026-09-20T14:51Z), PR head dffae8995cf206c6fd1e21b66f5a50df7e5decab. ⛔ Every reading below is this seat's own; the report was read, then checked, ⛔ never taken as the finding.

    ⭐ The discrimination this card turned on — verified structurally, ⛔ not by restatement

    The card, the triage ruling and the dispatch order all said the same thing three ways: two occurrences of the same symbol, only ONE of them a defect. So the review's first job was to prove the untouched one really is untouched.

    occurrence reading taken result
    docs/adr/0030-…:80 — the P0 — Seams shipped-history row git show origin/main:… vs git show origin/pr-19381:…, same line window, both printed byte-identical ⇒ ⛔ not in the diff at all
    docs/adr/0030-…:105 — the imperative cut-over bullet read in the diff struck in place, replaced with the withdrawal + its consequence + a link to the handoff tombstone

    ⭐ The structural reason the split is correct, and it is stronger than either line's wording: the two live under different headings — ### Shipped (merged to main), a per-phase table whose every row carries the PR that shipped it (#1434), versus ### Remaining work (handed off to a follow-up agent), an imperative list of steps someone is told to perform. A fix that scrubbed both would have rewritten a release record to make a grep pass.

    The amendment on the Status line — the one judgement call, and it holds

    The dev added one · **Amended** (2026-09-20, #16194 — …) entry. This seat checked it three ways rather than accepting the citation:

    1. The directive says so in as many words. AGENTS.md Prime Directive [WIP] Add Chinese version of the documentation #13: reversing a recorded decision "needs a new ADR (or an amended status line on the old one), not a changeset that quietly does the opposite." The amended status line is the directive's own named alternative, ⛔ not an invention.
    2. The form matches the register's own precedent. git grep -l '^\*\*Status\*\*.*Amended' -- docs/adr/*.md returns ten ADRs; the rendered form · **Amended** (date, #issue — …) is theirs verbatim.
    3. Every factual claim in it was checked against the retirement's own record, .changeset/retire-adr-0030-notification-event-migration.md: "zero production callers", "no way to be run", "both ways of giving it one — an os migrate sub-command and a boot-time invoker — were refused", and "pre-ADR-0030 sys_notification rows are not carried by the platform on this line" are each that changeset's own words. ⇒ the amendment records a ruling made elsewhere; it does ⛔ not make one.

    ⚠️ The PR body flags this line as the first to strike if a smaller diff is wanted. This seat's opinion, recorded for the maintainer: keep it. Without it the status line still reads **P0–P3b2 shipped** with no trace that a shipped P0 item was withdrawn — a reader who greps the status learns the opposite of what happened.

    ⚠️ The widening — TAKEN, declared, and it clears all four conditions

    The dispatch order refused to rule 5611847419 in or out and required the decision be made out loud. It was: docs/adr/0052-audit-is-not-the-activity-feed.md is in the diff, named in the PR body with its evidence. The adjacent-fix test, run by this seat rather than read off the report:

    condition reading
    same defect class ✅ a present-tense claim about what the platform does today, naming a path deleted from disk — the :105 side of the card's own test
    mechanical ✅ one sentence; the dev explicitly declined the judgement half (⛔ does not decide whether the ownership move proceeds) and ⛔ did not amend ADR-0052's status line, because no decision of ADR-0052 moved
    claimed by nobody else ✅ all 30 open PRs' file lists read (GET /pulls/{n}/files, ⛔ not titles): exactly one other PR touches docs/adr/** — #19322, on ADR-0090. search_issues for the filename over open issues: 0.
    same gate family ✅ both paths are docs/adr/**; the 19 derived families are identical for the two files

    ⭐ And the claim surface covers it: this round's claim declared File surface: docs/adr/ — the directory, ⛔ not the single file.

    ⚠️ One tension this seat has to name rather than paper over

    The triage ruling on this card (5608380082) wrote 「本卡的 PR 只能是一次 docs/adr/0030-notification-platform-convergence.md 的单文件改动」. The delivered PR is two files. That is not a breach, and here is the reasoning rather than an assertion:

    • The constraint's own stated hazard is the next clause — 「⛔ 不得与任何代码改动同 PR」 — i.e. dragging an otherwise-ordinary diff into hand-merge-only territory. A second governed ADR file creates none of that hazard: the diff was already governed, already Tier H, already hand-merge-only, and the gate family is byte-identical.
    • The widening request (5611847419) is later (2026-09-10 02:41Z vs 2026-09-09 20:37Z), comes from the domain:cli execution PM seat at refactor(metadata,spec)!: retire the adr-0030-notification-event migration — no operator door, no platform invoker #17194's ACCEPT, and routes the line to this card by name, having declined to file it as a second one.

    ⇒ the single-file phrasing was written before the second line existed, and the interest it protects is untouched. ⛔ Recorded here rather than resolved silently, because the maintainer is the one merging this and should not discover the discrepancy in the diff.

    Scope, changeset and gates

    • Scope: 2 files, +16 / −7, both docs/adr/**. ⛔ No content/docs/releases/ change, no package touched, no file unrelated to the card.
    • Changeset: skip-changeset was warranted and correctly NOT applied by the dev — the dispatch write budget named no label, and the dev said so instead of taking the write. The seat applied it at 2026-09-20T14:52:31Z; label read-back: documentation, size/s, skip-changeset. ⭐ The label is this repo's answer, ⛔ not an empty changeset (空 frontmatter changeset 相对 skip-changeset 标签零收益、单向风险 —— 「禁止空 changeset 进 .changeset/」的决策证据(#5292 结案后无处存放) #5471 ruled that route shut for a file a PR newly introduces).
    • Gates: 19 families derived from the actual changed files, 19 run, 0 NOT-MEASURED — a derived zero, every one of them recording an exit code and none of them 3. The one that first exited 3 — PREREQUISITE NOT MET (check:doc-formula-expressions, @objectstack/formula + @objectstack/lint unbuilt) was ⛔ not recorded as a finding; the packages were built under the shared verify lock and the gate re-run to 0. That is the right handling of a 3.
    • Repo-wide eslint: a measured narrowing, ⛔ not a skipped run — population read from eslint.config.mjs itself (no files: block matches Markdown), a count over both changed files (0 errors, both "File ignored"), and a non-influence argument (no block sets parserOptions.project/projectService, so type-aware linting cannot move a verdict on an untouched file). This seat accepts all three legs.

    Closing-keyword two-read — done by hand, and why

    node scripts/check-closing-keyword-parity.mjs --body <the PR body> — the instrument built for exactly this question — exited 3, PREREQUISITE NOT MET in the shared checkout (yaml uninstalled). ⚠️ NOT MEASURED, ⛔ not a pass. Substituted with two readings that were:

    1. By hand over the fetched body, GitHub's own grammar (all nine keywords, optional colon, #N / owner/repo#N / URL): exactly one binding — Fixes #17193, line 1. Every other #N in the body sits behind a non-keyword word — the card numbers #17194, #16194, #1434, #5471 behind PR/the/(, and #13/#14 behind Prime Directive, where they are directive numbers rather than cards at all.
    2. CI's own parse, which is the authority: The card this PR closes must claim this branch = success and No other open PR may claim the same issue = success.

    Fixes is the correct verb: all three bullets of the card's "Suggested shape" are delivered (:105 rewritten · :80 left · the status-line question decided), so the merge should close the card.

    Report hygiene

    • mcp_calls: 0. ⛔ No named write tool appears. API writes = 2, exactly the dispatched budget: the draft PR and the report comment. ⛔ No label write, ⛔ no PATCH of the PR body.
    • Its three API reads (GET issues/17193, GET issues/17193/comments, GET pulls/19381) are ⛔ not git-answerable — issue prose and a PR read-back have no local source. Clean on the git-instead-of-API criterion; recorded here because the checklist asks the question of every report, ⛔ not only of failing ones.
    • The one disclosed deviation worth naming: the first POST /pulls returned 415 (no Content-Type) and created nothing; it was resent and returned 201. ⭐ One PR exists — verified. The dev named the failed attempt rather than hiding it, which is the behaviour this seat wants.
    • Commit trailers carry the model-free form AGENTS.md mandates. A grep for a model identifier over the commit message and the PR body returns zero. One footer in the body.

    ⛔ Landing — the four-part route, ⛔ not the queue

    node scripts/pm/check-governed-merges.mjs --pr 19381 answers with its GOVERNED code, ⛔ not a prerequisite refusal:

    governed-surface predicate: 2 of 2 path(s) hit the register (6 surfaces, repo-agnostic).
      ⛔  GOVERNED — a human merge is the review record for this PR (#9495 regime).
          ⚖️ landing tier: H(人合) — the maintainer's hand, or an authorized APPROVED review
      size: 23 changed line(s) (+16 / -7) ≤ 5000 — under the human-merge threshold
    

    ⇒ this PR stays draft. ⛔ This seat will never flip it ready, never enqueue it, never arm auto-merge, and never approve it. The four parts, all performed this act:

    1. ① this review record, on the card;
    2. ② needs-user-decision on PR docs(adr): ADR-0030's cut-over step and ADR-0052's rollout note record the retired migration #19381 — ⚠️ on the PR, ⛔ not on this card: the six card states are mutually exclusive and ADR-0030 still prescribes migrateSysNotificationToEvent, a call the #16194 retirement removed — an operator copying step 2 gets an unresolvable import #17193 stays pm:dispatched until the merge closes it;
    3. ② the final Chinese 「维护者速读」 posted as a comment on the PR, this seat's correction of the dev's draft with 席位意见 filled in;
    4. ③ review requested from both authorised accounts, and ④ the round report lists it under awaiting a human merge.

    Recorded, ⛔ not fixed — with a 承接者 on each

    • docs/adr/0030-…:57 — the ## Phased delivery plan line still reads re-model sys_notification to the event (with migration). It states what P0 was planned to be, in a list the shipped table below then answers, and it names ⛔ neither the symbol nor a path, so nothing is copyable from it. ⇒ the :80 side of this card's own test. 承接者:无 — it moves only if someone rewrites ADR-0030's phase plan wholesale.
    • docs/adr/0052-… status line still reads sys_notification move deferred (P1/P2). ⭐ Still true as a statement of disposition — the move is still deferred — so it is ⛔ not stale and ⛔ not a finding. Recorded only because this PR changed the reason behind it. 承接者: whoever takes up ADR-0052 P0b.

    CI — converged, and ⚠️ the first reading of it was mine to correct

    Pinned to PR head dffae8995cf206c6fd1e21b66f5a50df7e5decab by construction (every poll went to GET /commits/dffae899…/check-runs, ⛔ never to a run list that could answer about another commit). Head re-read at the end: unchanged.

    surface reading
    check runs, latest per name 31 — 21 success, 10 skipped, 0 failing, 0 pending
    the two required jobs Lint & Repo Gates = success · TypeScript Type Check = success
    commit statuses (a different API) Vercel = success, combined = success
    PR object mergeable: true · mergeable_state: **clean** · draft: true

    ⚠️ This seat's own instrument had a hole, and it is on the record rather than quietly patched. The first CI monitor polled check runs only. Vercel is a commit status — a different endpoint — so a monitor that saw 31 green check runs reported "converged" while the combined status was still pending and mergeable_state read unstable. ⇒ a green reading from an instrument that cannot see the failing surface is a statement about the instrument, ⛔ not about the tree. Re-armed over /status as well, and the verdict above is the union of both.

    ⭐ The lit control that made Vercel=pending legible rather than dismissible: the head commits of the five most recently merged PRs (#19351, #19363, #19364, #19353, #19284) all read Vercel=success. ⇒ pending is ⛔ not this repo's resting state for that status; it was genuinely still running.

    One stale row a reader of the raw list will see

    The raw check-runs list holds 35 entries for 31 names. Among the duplicates, Check Changeset appears twice: 106096100747 = **failure** in check suite 96168673891-era run (before the label) and 106096624768 = **skipped** in the suite the skip-changeset write triggered. ⇒ the older row is the gate having worked, ⛔ not a finding and ⛔ not something to re-run away; GitHub's merge requirement reads the latest run per name, which is why the PR now answers clean.


    Generated by Claude Code

  9. removed their assignment
    on Sep 20, 2026
  10. huangyiirene commented on Sep 20, 2026

    @huangyiirene
    Collaborator

    LANDED — PR #19381. ⭐ And the way it landed is the part worth recording: the approver took it into the merge queue, not the seat.

    domain:engine#1, PM loop round 10, session_01NcPSwnmJHczmTu6FG7NMjE. Written 2026-09-20T23:13Z. Every reading below is this seat's own, taken in this act on a freshly fetched origin/main.

    reading result
    squash on origin/main 8e368dc3b990c3a86bbbbbf5b578d62c17d4c91f — %p word count 1, --is-ancestor origin/main ⇒ YES
    content — the defect ADR-0030 :105 now reads ~~Run \migrateSysNotificationToEvent` during the cut-over…~~**Withdrawn (#16194)**, followed by what that means for an operator (pre-ADR-0030sys_notificationrows are **not** carried) and a link to theData migration — RETIRED` tombstone
    control — the shipped-history row P0 — Seams (:80) appears 0 times in the diff hunks, with a lit control of 3 hits for migrateSysNotificationToEvent in the same hunk-only grep ⇒ the grep read the diff
    extent 2 files, +16 / −7 — 0030 and 0052, the second being the widening the dispatching seat asked to have decided out loud

    ⇒ the copy-the-example-and-it-fails defect is gone, and the record of what a past release shipped is byte-for-byte intact. That split was the whole card, and it held.

    ⚠️ A control of this seat's own that did NOT discriminate — recorded, not buried

    The first content probe asked for the old imperative wording to count 0. It counted 1, and that was not a landing failure — the text survives inside the strikethrough, which is exactly the shape the fix chose. ⇒ ⭐⭐ a control must be able to tell apart the two cases it is invoked to tell apart, and 「the old words are absent」 cannot distinguish 「prescription removed」 from 「prescription withdrawn in place」. The reading that does the work is the structural one above: the withdrawal marker and the consequence sentence are present, and the shipped row is absent from the diff.

    ⚠️ This is the second time this session that a negative control of this seat's was too coarse to answer its own question. Both were caught, and both are written down where the next reader meets them.

    ⭐⭐ The governed route: performed in full, then overtaken by an approver — as it happened

    This PR touches docs/adr/** ⇒ governed ⇒ this seat performed all four parts and ⛔ flipped nothing:

    1. ACCEPT on this card (5750618569);
    2. needs-user-decision on the PR, ⛔ not on this card — the six card states are mutually exclusive and ADR-0030 still prescribes migrateSysNotificationToEvent, a call the #16194 retirement removed — an operator copying step 2 gets an unresolvable import #17193 stayed pm:dispatched until the merge closed it;
    3. the final Chinese 维护者速读 (5750620594);
    4. review requested from os-zhuang and hotlong.

    Then, read from the PR timeline in this act:

    2026-09-20T22:54Z → 2026-09-20T22:55Z event actor
    22:54:29Z reviewed — approved os-zhuang
    22:54:34Z ready_for_review os-zhuang
    22:54:39Z auto_merge_enabled os-zhuang
    22:55:20Z added_to_merge_queue os-zhuang

    ⇒ ⭐ A requested approver, with the authority this seat does not have, chose the queue over a hand merge. That is their act and their prerogative. ⛔ The seat did not flip the draft bit, arm auto-merge, enqueue, or approve — and it does not undo an approver's decision either. It is recorded here plainly rather than smoothed into 「it landed」, because a future reader comparing the governed rule (「a human merges it」) against this card's history would otherwise read a breach into it.

    ⚠️ For the next governed card in this lane: the seat's four parts are unchanged. The route did not become 「flip it ready and wait」 because it happened to be overtaken once. ⛔ Never infer a rule change from someone else's exercise of their own authority.

    Closeout

    Fixes #17193 closed this card on the merge (completed, 23:12:09Z) — ⛔ but an auto-close does not remove the state label. Taken off by hand in the same act: pm:dispatched removed, assignee cleared, read back clean ⇒ documentation · domain:engine · priority:p3, 0 assignees.

    ⛔ domain:engine and priority:p3 are left exactly as triage set them — ⛔ never a seat's to write.


    Generated by Claude Code

  11. added a commit that references this issue on Sep 28, 2026
    8e368dc
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions