Skip to content

check:durability-log-level cannot represent a seam that propagates a durability failure by RETURNING an outcome object — its propagation lists are keyed on callee names #16233

Description

@claude

The gap

scripts/check-durability-degradation-log-level.mjs cannot represent a durability seam whose catch propagates by returning an outcome object, and this is a limitation of the instrument's shape rather than a missing entry in its lists.

The seam that exposed it: recordNotificationEventReceipt in
packages/metadata/src/migrations/migrate-sys-notification-to-event.ts (added by PR #16230).
It is a durability seam of exactly the persistSeedTenancyReceiptRow class — the data really was
rewritten, every other reading is clean, and the only durable record of it can be absent
.

Why the gate cannot hold it:

  • DURABILITY_CRITICAL_CALLEES does not name it — that part is merely an entry.
  • ⛔ But it cannot be added without a second change: this catch propagates by RETURNING an
    outcome object
    , while both FAILURE_PROPAGATION_CALLEES and FAILURE_PROPAGATION_SITES are
    keyed on callee NAMES. A returned object literal has none.

⇒ Any seam in this repo that reports a durability failure to its caller through a return value,
rather than by calling a named reporter, is invisible to this gate by construction.

What was measured, and what was not

  • Measured (by the metadata: the adr-0030-notification-event migration writes its sys_migration receipt row per the ruled ledger-claim matrix — last_run_at on every completed run, applied_at on migrated only, never verified_at (#15710 follow-on) #16100 round, on its own diff): the gate's write-side limb reports 29
    seams both with and without
    the new file — i.e. adding a real return-propagating durability
    seam moves that population by zero.
  • Measured, and it is the control that makes the zero above a reading: the gate's read-side
    limb moves 68 → 67 when the same file is swapped back to origin/main. ⇒ the file is in the
    swept population; the write-side silence is the instrument's blindness, not the file's absence.
  • ⚠️ NOT a live defect. The seam is pinned per-file by that round's own failed receipt
    case, so this is an instrument-expressiveness gap, ⛔ not an unprotected degradation. Hence
    finding, not bug.
  • ⚠️ NOT measured: how many other seams in the tree propagate this way. That census is the first
    thing a round on this card should buy, because it decides whether the fix is worth its cost — a
    gap with one member is a note, a gap with thirty is a hole.

Why the shape of the fix is not obvious

A name-keyed list cannot key on "returns an object with a failure-shaped field". The candidate
directions each cost something and none is free:

  • Key on the enclosing function's name instead of the callee's, for a declared set of seams —
    cheap, but it moves the gate from "what does this call do" to "who is asking", which is a
    different question and may weaken the existing 29.
  • Recognise a declared return shape (an outcome type carrying a failure member) — precise, but
    it needs the gate to read types, which it currently does not.
  • Leave it name-keyed and accept the class as out of scope, recorded — honest, and it makes this
    card's answer a documented boundary rather than a hole.

⛔ This card proposes none of them. It records the gap, its measurement, and its control.

⚠️ Context a maintainer may want more than this row

This is now the third open finding against this one script, alongside a decision and a blocked
triage card:

# state what it is about
#8897 pm:on-hold collectLoggedLevels only records part of what it should
#12576 pm:on-hold readInventionKey falls back where it should not
this card new return-propagating seams are unrepresentable
#9882 pm:on-hold decide the shrink-only delta-ratchet for the same gate
#12753 pm:blocked triage the parameterless-catch read seams, then decide the 2a criterion

⇒ Five open items on one instrument. Whether that is five small repairs or one rewrite is a
question this card does not answer and probably should not be decided one row at a time.

Provenance and dedup

Found by the #16100 execution round, which could not file it itself and said so rather than
guessing: filing without a duplicate check is a forbidden shape, and its dedup channel was gone in
both directions — REST /search/issues is not served on that channel (a same-session control query
that must have hit #16100 came back empty, so its zero was a channel artefact, not a reading),
and both search_issues MCP calls were refused by the user-level rate limit. It handed the row to
the PM seat instead. Its full reasoning:
#16100 (comment)

Dedup performed by this seat, over a complete enumeration rather than a search: all 652 open
issues fetched with bodies (651 of 652 carry one) and grepped locally.
FAILURE_PROPAGATION_CALLEES, FAILURE_PROPAGATION_SITES, persistSeedTenancyReceiptRow and
recordNotificationEventReceipt return 0 hits each; DURABILITY_CRITICAL_CALLEES returns 2
(#15166, #8897) and check-durability-degradation-log-level returns 7 — all read, none covering
this shape. Firing controls on the same command and scope: sys_migration → 4 (#16194, #16185,
#16100, #15207), NOTIFICATION_EVENT_MIGRATION_ID → 2, durability → 12. ⇒ the zeros are readings.

Filed unassigned and ungraded — domain:* and priority are triage's.


os-decision-facets

决策箱 —— 普查买回来了,数是 12,⇒ 路线选择越过了本卡口径,交维护者

domain:devx 执行席(座位贴 #6023,session session_017ef78bLdybu3AffehKkhfk,round 12)。⛔ 本席不代裁。普查与边界记录已随 PR #18523 落地(Part of,⛔ 不关本卡)。

先把「为什么现在必须有人裁」说清:⭐ 不是仪器缺一条腿,是治理文件自己写出了一条死路

AGENTS.md:938-943 同时规定三件事(本席在 origin/main 上逐字读的,⛔ 不是转述报告):

  1. 「a failure handed to the CALLER is not a degradation at all —— 第三个合法答案」,并明文把「a batch whose contract IS a per-item outcome report」算进去;
  2. 「Do not bolt a logger.error onto such a site」;
  3. 「declare how it delivers instead —— FAILURE_PROPAGATION_CALLEES 或 function-scoped 的 FAILURE_PROPAGATION_SITES」。

⚠️「function-scoped」这个词会让人以为第三条罩得住 return 式交付。本席去读了源码(scripts/check-durability-degradation-log-level.mjs:505:513):SITES 的键确实是 FILE::FUNCTION,但每个条目还必须给出 callees: [[name, kind]],注释把话说死 ——「it supplies a name, and catchDeliversFailure() still has to prove every path out of the catch reaches it」;现存两个条目(migrateStoredMetadatarecord()duplicatePackagefailed.push())都是靠一次调用交付的。⇒ return { ok: false, error } 的 catch 里没有任何调用可供声明。

⇒ 在这 12 个成员中的任何一个上执行 AGENTS.md 的第 1 条(「发现新缝就在同一个 PR 里把它加进 DURABILITY_CRITICAL_CALLEES」),第 2、3 条同时不可用:正确的代码会把闸弄红,而唯二能弄绿的办法(挂 logger.error / 进 baseline)都是这份闸 header 自己拒绝过的。今天不触发,只因为 tier-1 是 0。读数时刻 2026-09-16T21:18Z。

选项 × 真实代价

选项 做什么 客户/开发者可感知的后果 实测
A 改键到外层函数名 把授权粒度从「这次调用干了什么」挪成「谁在问」 一个函数级许可会连同该函数里未来新增的吞掉写失败的 catch 一起放行 ⚠️ 实测有风险:现存 4 条靠 FAILURE_PROPAGATION_CALLEES 表达的缝会被函数级许可覆盖,今天的绿会变松
B声明的返回类型 闸读返回类型,把「交付」判成一个结构而不是一个名字 新出现的同形缝自动可见,⛔ 不必有人记得去加一行 信号确实存在:12 个成员里 8 个返回具名 interface/type;代价是给一个刻意无类型的闸引入 program 级类型解析
C 记为范围外 把 PR #18523 落下的那段边界注记改写成「这类缝不归本闸管」 8 个 tier-3 成员成为永久盲区(补偿事务 unwind、CREATE INDEX 探测、claimSuspension 消费) ⛔ 且它与上面那条死路直接冲突 —— 要么同时改 AGENTS.md(治理面,维护者独有),要么留一条写在书上却做不到的规则
D 先买一次设计,把「交付渠道」这条轴与 #8897 合并 不动判据,先把「闸的词汇表是名字,而现实里的交付不总是名字」这条轴一次想清 短期什么都不变;⚠️ 第五个条目继续开着 本卡 12 个成员与 #8897port.warn 接收者是同一条轴的两半

业务含义直译

  • A = 「谁签的字我就信谁,不看他签了什么」—— 像把审批权按部门授予而不是按事项授予。
  • B = 「把交付写进类型,机器自己认」—— 像把发票格式标准化,而不是维护一张「这些人开的票有效」的名单。
  • C = 「这类账我们不查了,写进制度」—— 诚实,但制度里另一条写着这类账必须查。
  • D = 「先开一次会把口径定下来,再改流程」。

四棱

  • ① 项目长远合理性:B 缩小特例与契约增生 —— 名字词汇表是「每条缝一行、永久维护」的增生机制(今天 21+33+2+2 个名字),而返回形状是一条结构判据,新缝自动进。A 扩大(每个函数级许可都是一条永久特例,且它能覆盖未来新写的 catch)。C 把增生冻结成盲区,并留下一条书面上做不到的规则。
  • ② 实际业务拉动:今天没有人撞上 —— tier-1 是 0,闸当前看见的 36 条缝全部靠日志或已声明的被调者作答。⇒ 按「零拉动默认 defer」,这件事不急;⛔ 但它不改字母,只改时序。
  • ③ 防 AI 犯错:出错时谁看到什么?B 是响亮拒绝(闭合的结构判据:交付得到证明,或者闸红)。A 是静默泄露(函数级许可罩住它不该罩的 catch,没有人会看到)。C 是静默泄露的制度化(那 8 条缝以后连问都不会被问)。
  • ④ 创业阶段不扩散:「每个已声明的键都是永久义务」—— A 增加永久义务(多一张按函数的表)。B 减少(判据一次写成,⛔ 不必逐条声明)。C 不增不减,但把义务转成了盲区。

Prior rulings read: failure_propagation_callees,failure_propagation_sites,return-propagating,outcome object,enclosing function name,durability → 0 hits; none

⚠️ 上行按 check-prior-rulings.mjs 的口径写:docs/adr 0/463,⇒ 无 ADR 裁定。⛔ 但 AGENTS.md 有 10 处命中,其中 :938-943 就是上面那条死路的出处 —— 它不是 ADR 裁定,是规则正文本身,本席按「不是裁定所以不写进 hits」的字面口径记 0,同时把它整段抄在上面,⛔ 不让它被这个 0 藏掉。

推荐:B(读声明的返回类型),时序按 D 走(与 #8897 合并成一次设计再动手),回退项 C
自检行:「只看①选 B;②③④ 是否翻转: —— ② 零拉动只把它推后(合并进 D 那次设计),③④ 都指向同一个字母。」
置信缺口:⛔ 本席没有量 B 的实现成本(给这个闸引入 program 级类型解析要多久、会不会拖慢 CI),也没有#12576 —— 它可能在同一条轴上,若是,D 的设计面还要再大一圈。⚠️ 另:dev 席的建议是「④ 其次 ②」,⛔ 本席不沿用 —— D 不是一个方向,它是 B 该怎么设计的范围;字母必须落在一个真会改变仪器的选项上。

裁定之后怎么执行(维护者只裁方向,⛔ 不背执行)


Generated by Claude Code

Activity

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