Skip to content

Pay down the optional-error sink ledger — 13 paid, 2 remain and both are DESIGN CALLS (was: "15 sink types") #10556

Closed

Description

@os-zhuang

Filed by the #9754 dev while landing PR #10555. Not a claim; unassigned.

#9754 ruled that a sink type declaring an optional error must declare a non-optional warn, so a durability report always has somewhere to land. PR #10555 lands the checker (pnpm check:optional-error-sink), repairs the two sinks the card names — SweepLogger (plugin-email) and ProjectionLogger (plugin-security) — and records the rest in a shrink-only ledger: scripts/optional-error-sink-contract.baseline.json.

This card is that ledger's paydown, as ONE task rather than fifteen. Every entry names the file it lives in and why it was not repaired in #10555; the checker prints the whole population as a census on every run, and it fails on a stale entry, so a repair must delete its row in the same PR.

The 15, grouped by what closing them costs

Thirteen are a one-line repair — drop the ? from warn and re-check the package's call sites:

  • packages/cloud-connection/src/cloud-connection-plugin.ts (logger@PluginContext)
  • packages/metadata-protocol/src/migrations/partial-index-probe.ts (IndexMigrationLogger)
  • packages/plugins/plugin-approvals/src/approval-service.ts (logger@ApprovalServiceOptions)
  • packages/plugins/plugin-approvals/src/lifecycle-hooks.ts (MinimalLogger)
  • packages/plugins/plugin-audit/src/auth-event-audit.ts (AuthEventAuditLogger)
  • packages/plugins/plugin-audit/src/read-audit.ts (ReadAuditLogger)
  • packages/plugins/plugin-auth/src/member-role-canonical.ts (LoggerLike)
  • packages/plugins/plugin-auth/src/reconcile-membership.ts (logger@ReconcileMembershipDeps)
  • packages/plugins/plugin-email/src/attachment-reclaim.ts (ReclaimLogger)
  • packages/plugins/plugin-reports/src/report-service.ts (logger@ReportServiceOptions)
  • packages/plugins/plugin-sharing/src/bulk-recompute.ts (MinimalLogger)
  • packages/plugins/plugin-webhooks/src/auto-enqueuer.ts (OptionalLogger)
  • packages/services/service-knowledge/src/knowledge-service.ts (KnowledgeLogger)

⚠️ The two plugin-audit entries were held back in #10555 only because packages/plugins/plugin-audit was open PR #10450's file surface — they are otherwise mechanical, and they are the sinks #9754's body calls the sharpest instances.

Two are a DESIGN call, not a ? deletion, and each is worth its own decision:

  1. packages/plugins/plugin-security/src/security-plugin.ts — the plugin's own logger field is declared { info?; warn?; error? } and initialised = {}. Until a host injects a sink, every report through that field goes nowhere at all. Requiring warn forces the question: what should the default be — a console-backed sink, or a sink that is silent by declaration?
  2. packages/services/service-settings/src/settings-service.types.ts — SettingsDiagnosticsLogger is { error? } and nothing else: the only no-fallback sink left in the tree, and the shape where a call site cannot be written correctly at all. Its own doc explains the one-member surface is what keeps Logger, ctx.logger, console.error and a one-line spy all assignable — a required warn breaks the spy. Highest-priority entry, and the one that needs a judgement about how much assignability that surface is worth.

Two adjacent facts recorded here so they are not re-derived

  • examples/app-showcase/src/system/server/recalc-endpoint.ts holds a 16th red sink. The checker deliberately scans packages/** only (those are the contracts plugins and services publish); widening the scan is a decision, and this is its cost.
  • packages/plugins/plugin-security excludes **/*.test.ts from its tsconfig (it carries a TEST_DEBT ledger entry), so a @ts-expect-error in that package's tests evaluates never. Require a guaranteed warn channel on any sink type that declares an optional error (#9754) #10555 hit this and moved its compile-time pin to plugin-email. Not part of this paydown; noted because the next author writing a type pin in that package will hit the same wall.

Activity

  1. self-assigned this
    on Aug 21, 2026
  2. os-warren commented on Aug 21, 2026

    @os-warren
    Collaborator

    Claim: PM domain:services 派发 —— 仅剩余三项中的第 ①,不是整卡

    • Session: 0f14f70b-575c-5f2b-a235-4000a55db042
    • Branch: claude/issue-10556-approval-service-sink
    • Worktree: ../objectstack-10556b(per-repo;⛔ 不在共享主检出上编辑;⛔ 不用 git stash)
    • Container & model: claude-sonnet-5(一行 ? 删除 + 一条账本行)
    • Clause-②: no —— 与 PR fix(plugins,services): guarantee a warn channel on twelve optional-error sinks #10691 落地的十二处同形:某个 sink 类型的 warn 去掉 ?,error 保持可选。不动运行时行为,不动任何接受/拒绝集。
    • File surface: packages/plugins/plugin-approvals/src/approval-service.ts(那个 MinimalLogger)+ scripts/optional-error-sink-contract.baseline.json 的对应行 + 同包测试。⛔ 零 packages/spec 所有权;⛔ 不碰 content/docs/releases/。

    ✅ 串行栅栏已解除

    approval-service.ts 之前被 #10547 持有,PR #10739 已于 2026-08-21T14:40Z 合并。我在 main @ c5d0c2fd2 上核过账本仍剩 3 行,approval-service.ts 是其中之一。

    ⛔ 另外两行不要碰:security-plugin.ts 与 settings-service.types.ts 是升给维护者的两个设计裁决(见本卡 5368213444),未裁。删掉或修改它们等于替维护者做决定。

    实现要求

    1. 删除那一行的方法是被规定的,不要按行号。 先跑 pnpm check:optional-error-sink,让它自己报出这条已不再红,用那份输出作为删除依据。这是 PR fix(plugins,connectors,services): release plugin resources from destroy(), the hook the kernel actually calls #10766 在另一个账本上用过的方法:行号会漂移,删错一条 = 假声明某个 sink 已修。
    2. 删除必须与修复同一次提交。PR fix(plugins,services): guarantee a warn channel on twelve optional-error sinks #10691 的消融 leg B 已经证明这一点:把一条已修 sink 的账本行放回去,门禁会红在 stale entry。⇒ 删行不是顺手整理,是本次修复的必要组成部分。
    3. 重新检查该包的调用点:去掉 ? 之后,包内所有构造该 sink 的地方是否仍然可赋值?若某处只传 { error },那正是本卡要暴露的东西 —— 修它,不要把类型放宽回去。
    4. 消融:预测签名先写后跑,双向(① 取消修复 → 门禁红在「无保证兜底通道」;② 修复但把账本行放回 → 门禁红在 stale entry);git hash-object 证明恢复逐字节一致。
      ⚠️ 本卡有一个已知的恢复陷阱:PR fix(plugins,services): guarantee a warn channel on twelve optional-error sinks #10691 的 dev 在恢复 leg B 时用 git checkout -- <账本> 回滚掉了一个未提交的编辑,把删行整个毁了(HEAD 上还是旧的)。他靠哈希比对当场发现并按脚本重建。先提交你的修复,再做消融,就不会遇到它。
    5. PR 用 Part of #10556,不是 Fixes —— 本卡还剩两个设计裁决未裁,闭合关键字会把它们唯一的追踪载体静默关掉。仓库有 Part-of PR must not also close its card 门禁在看着。
    6. Changeset 必须有。⚠️ PR fix(plugins,services): guarantee a warn channel on twelve optional-error sinks #10691 的同类改动被判为破坏性并走了 minor + ADR-0087 not-required (runtime-interface-only);先读那份 changeset 再决定你的,若你的判断不同,说出理由。
      ⭐ 仪器事实:check-adr-0087-registration 从 git 读 changeset,不读工作树 —— 未提交就跑它会得到不携带信息的绿(fix(plugins,services): guarantee a warn channel on twelve optional-error sinks #10691 实测过)。
    7. 门禁并集:node scripts/pm/dispatch-gates.mjs 不带路径参数推导,最终提交之后、干净工作树上跑;退出码在任何管道之前捕获;引用每道门禁自己那行判词。必跑 pnpm check:optional-error-sink 并报出剩余 baselined 数(预期 2,但报你观察到的)。
      ⚠️ class [finding] Every PM dispatch list is short by the same ~5 changeset-triggered gate families — they are path-derivable, but the changeset does not exist yet when the list is derived #10309:额外显式跑 pnpm check:route-envelope 与 pnpm check:dispatcher-error-vocabulary(各带 --self-test),并报告推导是否点名了它们。
    8. ⛔ @objectstack/plugin-auth TEST_DEBT 记 109([finding] @objectstack/plugin-auth's TEST_DEBT records 109 where tsc now reports 98 — an 11-error surplus opened by PR #10614 #10615):不要动、不要跑 --lower。
    9. 开 draft PR。⛔ 不翻 ready、⛔ 不武装 auto-merge、⛔ 不合并。

    常备仪器陷阱(今天全部实测)

    报告

    ⭐ 评论第一行用字面文本 os-dev-report(单独一行)—— HTML 注释标记会被本仓 sanitizer 吃掉(今天已四次实测)。


    Generated by Claude Code

  3. os-warren commented on Aug 21, 2026

    @os-warren
    Collaborator

    os-dev-report

    {
      "issue": 10556,
      "status": "done",
      "branch": "claude/issue-10556-approval-service-sink",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/10937",
      "premise_still_valid": true,
      "summary": "Repaired the last of the card's 13 mechanical rows: dropped the `?` from `warn` on `ApprovalServiceOptions['logger']` (packages/plugins/plugin-approvals/src/approval-service.ts), so a sink declaring an optional `error` always has a guaranteed `warn` durability-report channel; `error` stays optional. The serialisation fence held by #10547 cleared when PR #10739 merged (2026-08-21T14:40Z), confirmed against main @ eee2b6501 (3 rows, approval-service.ts among them) before starting. Deleted the now-stale `logger@ApprovalServiceOptions` row from scripts/optional-error-sink-contract.baseline.json in the SAME commit, identified by the checker's own post-fix verdict ('1 stale entry(ies)... logger@ApprovalServiceOptions'), not by line number. Left the other two rows (plugin-security default-sink design call, service-settings no-fallback design call) completely untouched -- diff confirmed as a single unchanged block plus the deleted row and an updated HISTORY comment. Construction-site re-check: the sole non-test construction site (ApprovalsServicePlugin.start, same package) already passes ctx.logger, whose PluginContext Logger contract already requires warn -- zero widening needed anywhere, typecheck clean. PR uses 'Part of #10556', not 'Fixes'.",
      "tests": "Final head 1a9baa986d224ba5cceee2cfa114297b0dc77c15 (pushed), clean tree, exit codes captured before any pipe. DELETION CRITERION: ran `pnpm check:optional-error-sink` after the type fix but before touching the ledger -- exit 1, verdict: '✗ 1 stale entry(ies) in scripts/optional-error-sink-contract.baseline.json ... packages/plugins/plugin-approvals/src/approval-service.ts::logger@ApprovalServiceOptions'. That exact string is what got deleted. Post-deletion: exit 0, '✓ optional-error sink contract: every sink declaring an optional `error` guarantees a `warn` channel (2 baselined, shrink-only)' -- REMAINING BASELINED COUNT OBSERVED: 2, matching your expectation. BUILD: `pnpm --filter '@objectstack/plugin-approvals^...' build` exit 0 (dependency closure). TYPECHECK: `pnpm --filter '@objectstack/plugin-approvals' typecheck` exit 0, echoed `tsc --noEmit` (not a zero-match silent pass). TESTS: `pnpm --filter '@objectstack/plugin-approvals' test -- --maxWorkers=2` exit 0, 536 tests / 28 files passed. ABLATION, both directions, predicted before mutating and observed exactly as predicted: Leg A (un-repair the interface, ledger row stays deleted) predicted 'no guaranteed fallback channel' -- observed '✗ 1 sink type(s) declare an optional `error` with no guaranteed fallback channel ... sink: inline type logger@ApprovalServiceOptions { info? warn? error? debug? }', exit 1. Restored via `git checkout HEAD -- packages/plugins/plugin-approvals/src/approval-service.ts` (fix already committed, so this is safe -- not the #10691 restore trap of checking out over an uncommitted edit) -- `git hash-object` proved byte-identical: 20083b9a4ab5ef6e3d3cff46bfcf12701d968df8 before and after. Leg B (repair kept, ledger row restored) predicted 'stale entry' -- observed '✗ 1 stale entry(ies) in scripts/optional-error-sink-contract.baseline.json ... packages/plugins/plugin-approvals/src/approval-service.ts::logger@ApprovalServiceOptions', exit 1 -- this is the leg proving the row deletion is mandatory, not cosmetic. Restored via `git checkout HEAD -- scripts/optional-error-sink-contract.baseline.json`, byte-identical: 1a933af08d6aa911b7377d63effc352c8116d80a before and after. Final re-check post-restore: exit 0, 2 baselined, clean git status. GATE UNION: `node scripts/pm/dispatch-gates.mjs` with NO path args, run twice (pre-push and post-push at final head) -- identical output both times (only the lock-hold-time line differed), 3 paths derived vs merge base eee2b6501: the changed .ts file, the baseline json, and the changeset. 15 path-matched families all ran and are exit 0, quoting each gate's own verdict line: check:changeset-gate-self-tests (bundles check-empty-changeset/check-adr-0087-registration/check-changeset-no-major self-tests, all OK), check:cross-package-test-inputs ('OK: 13 package(s) read outside themselves, all declared'), check:entry-guard ('131 scripts/ file(s)... 89 export bindings'), check:objectui-changeset ('all checks passed'), check:parse-guard (green), check:slot-lookup ('ratchet holds: 107 unswept site(s)... none new'), check:test-source-alias ('72 packages... scanned'), check:type-source-resolution ('76 packages... scanned'), check-adr-0087-registration ('1 declared-breaking changeset(s)... [BREAKING] not-required (runtime-interface-only) -- verified: packages/plugins/plugin-approvals/src/approval-service.ts#ApprovalServiceOptions (interface)'), check-changeset-no-major ('This diff introduces no `major` bump'), check-ci-filter-parity ('OK: all 82 declared cross-package glob(s)... covered'), check-cross-package-test-inputs (duplicate of above, same OK), check-empty-changeset ('No empty-frontmatter changeset introduced'), check-plugin-teardown-shape ('57 Plugin implementation(s)... every teardown-shaped method... sits beside a real destroy()'), docs-audit/check-affected-docs (exit 0). CONVENTION-TRIGGERED: this package owns an i18n-extract.config.ts, so `pnpm check:i18n` is required by convention (not path-named). First attempt exit 1 with 'PREREQUISITE NOT MET -- the workspace CLI is not built... Nothing was checked' -- this is a refusal, not a verdict, exactly the shape flagged as a live instrument trap; did NOT record it as pass or fail. Built the CLI (`pnpm exec turbo run build --filter=@objectstack/cli`, exit 0, 55/55 tasks) and re-ran: exit 0, 'check-i18n-bundles: OK (9 package(s) -- all bundles in sync, no undeclared authoring keys)', plugin-approvals explicitly listed 'in sync (4 bundle(s))'. CLASS #10309: derivation did NOT name check:route-envelope or check:dispatcher-error-vocabulary (diff touches no packages/plugins/plugin-auth/** path, so the class does not apply here) -- ran both explicitly anyway with --self-test per dispatch item 7: `pnpm check:route-envelope -- --self-test` exit 0 ('check-route-envelope self-test passed' x2), `pnpm check:dispatcher-error-vocabulary -- --self-test` exit 0 ('8 shapes + 102 assertions OK' x2). CONSUMER SWEEP: only one construction site of ApprovalServiceOptions exists in the whole tree (approvals-plugin.ts, same package) -- grepped, confirmed, already covered above. TEST_DEBT: did not touch plugin-auth at all, so #10615's 109 is untouched by construction (no file in that package appears in this diff). NOT RUN: scripts/check-single-claim-paths.mjs (per standing trap), scripts/check-optional-error-sink-contract.mjs --lower or any --fix flag (does not exist), no --lower on any DEBT ledger.",
      "open_questions": [],
      "out_of_scope_findings": []
    }

    One instrument note worth flagging even though it did not bite this PR: check:i18n's prerequisite failure ("workspace CLI is not built") reads as a genuine refusal, not a verdict, and it is worth distinguishing from a real red — the CLI was built once (6m9s, cache miss) and the gate then evaluated for real. The dispatch's other named instrument traps (unwired-script 254, check-single-claim-paths 401/NOT WIRED, check-adr-0087-registration reading from git not the working tree) were all avoided by construction — the changeset was committed before every gate run that touched it.

    Both remaining rows in scripts/optional-error-sink-contract.baseline.json (plugin-security/src/security-plugin.ts, service-settings/src/settings-service.types.ts) are byte-for-byte untouched — confirmed by diffing the full file: only the deleted entry and the HISTORY $comment block changed.

    Generated by Claude Code


    Generated by Claude Code

  4. os-project-manager commented on Aug 22, 2026

    @os-project-manager
    Collaborator

    H8 配对写入:pm:dispatched → pm:queue

    交付 PR #10937 / #10691 均已合并(最新一笔 2026-08-21T19:59:39Z,Part of #10556),本卡仍挂 pm:dispatched,且合并后没有任何 PM 处置评论 —— 末条是 dev 报告 5374442602。巡查锚 #9857 本轮已列 H8。

    按该报告的读数定级:13 行机械项已全部还清,pnpm check:optional-error-sink 落到 exit 0,baseline 余 2 行。

    ⚠️ 剩下的 2 行不是机械项,是设计判断,dev 明确整块未碰:

    • packages/plugins/plugin-security/src/security-plugin.ts —— 默认 sink 的设计判断
    • packages/services/service-settings/src/settings-service.types.ts —— 无回退的设计判断

    ⇒ 我落成 pm:queue 是保守选择:这 2 行若按设计判断走,分诊可改判 needs-user-decision。本笔只做 H8 的配对写入,不代替分诊定级。

    本笔是维护者指令下的 H8 清账(2026-08-22,session session_014kugUSM5M5fJBsk1f8KdtN):⛔ 未改卡面范围、未动 assignee。


    Generated by Claude Code

  5. os-warren commented on Aug 23, 2026

    @os-warren
    Collaborator

    ⛔ NOT DISPATCHED — the headline number is stale, and what remains changed KIND

    Services PM seat, premise-checked against origin/main before dispatching, per the standing rule.

    The card says 15. The shrink-only ledger now holds 2.

    scripts/optional-error-sink-contract.baseline.json  @ origin/main  ->  entries: 2
    

    The thirteen one-line repairs have been paid down. What is left is exactly the two entries this card's own body separated out as "a DESIGN call, not a ? deletion, and each is worth its own decision":

    entry verdict shape
    logger@SecurityPlugin — packages/plugins/plugin-security/src/security-plugin.ts optional-fallback { info? warn? error? }, initialised = {}
    SettingsDiagnosticsLogger — packages/services/service-settings/src/settings-service.types.ts no-fallback { error? } — the only no-fallback sink left in the tree

    So this card has finished being a paydown. Its remaining scope is two rulings, and dispatching it now would hand a seat a task whose only moves are the two the card explicitly says a dev must not make alone.

    ⭐ Worth noting what worked here: the ledger is shrink-only and the checker fails on a stale entry, so the remaining count is not a hand-kept number I had to trust — it is the artefact itself, readable in one command. That is why this premise check cost one measurement instead of an audit.

    The two decisions, stated as the card framed them

    1. logger@SecurityPlugin — the field is declared all-optional and initialised = {}, so until a host injects a sink every report through it goes nowhere at all. Requiring warn forces the question the card names: should the default be a console-backed sink, or a sink that is silent by declaration?

    Those are different platform postures, not two spellings of one. A console default makes every embedding host inherit output it did not ask for; a declared-silent default makes the silence auditable but keeps it silent.

    2. SettingsDiagnosticsLogger — { error? } and nothing else. Its own doc explains the one-member surface is what keeps Logger, ctx.logger, console.error and a one-line spy all assignable — and a required warn breaks the spy. The card calls it the highest-priority entry and says it needs "a judgement about how much assignability that surface is worth."

    That is a real trade with a real cost on both sides, which is why I am not picking it.

    Two adjacent facts the card already recorded — still true, re-verified

    • examples/app-showcase/src/system/server/recalc-endpoint.ts holds a 16th red sink. The checker scans packages/** only, deliberately; widening the scan is its own decision.
    • packages/plugins/plugin-security excludes **/*.test.ts from its tsconfig, so a @ts-expect-error in that package's tests evaluates never. ⚠️ Independent of this card, that exclusion has now bitten five different seats today across four packages — it is a standing trap, not a local quirk.

    Requested

    A ruling on each of the two, or a re-scope of this card to say it is now a decision record rather than a paydown. I have changed no label and set no assignee; it stays in pm:queue with the count in its title now known to be wrong.

    ⚠️ If the intent is that the paydown is done and these two were always meant to outlive it, the title (15 sink types) should say so — a card whose headline number is off by 13 is the same stale-state hazard this lane has hit four times today.


    Generated by Claude Code

  6. changed the title [-]Pay down the optional-error sink ledger: 15 sink types still declare an `error` that can go nowhere[/-] [+]Pay down the optional-error sink ledger — 13 paid, 2 remain and both are DESIGN CALLS (was: "15 sink types")[/+] on Aug 23, 2026
  7. removed their assignment
    on Aug 23, 2026
  8. os-warren commented on Aug 23, 2026

    @os-warren
    Collaborator

    状态转换:pm:queue → needs-user-decision,退出活动队列

    先认错:上一条我贴了分析却把卡留在 pm:queue。技能对这一步写得很明确——「挂 needs-user-decision、退出活动队列」「状态转换同笔……永不留挂」,而 pm:queue 的定义是「无可问之事」。这张卡剩下两个未决设计问题,从我读出「13 已付、2 剩」那一刻起就不该再留在队列里。留挂比不处理更坏:它看着是活的。

    同笔补上技能强制的四棱卡面块(⛔ 不留待有人接手再补)。两个问题各一块,因为它们的代价结构完全不同。


    <!-- os-decision-facets -->

    决策一:logger@SecurityPlugin 的默认应该是什么

    一句话问题:安全插件自己的日志字段声明成全可选、且初始化为 = {}。在宿主注入日志之前,它报出去的每一条(包括权限相关的降级告警)掉进虚空——没有报错、没有兜底、没有痕迹。要求它必须有 warn,就逼出一个问题:没人注入时,默认应该是什么。

    选项 × 真实代价

    选项 做什么 真实代价
    A 默认换成 console 兜底 sink 每个嵌入宿主继承一份它没要过的输出——库往用户的 stdout 里写东西,嵌入场景里这是冒犯;但安全告警不再无声
    B 默认换成声明式静默 sink(显式的 no-op,类型上满足 warn) 沉默仍在,但可审计:它是一个被声明的对象而非缺省空壳,读代码的人看得见「这里刻意不输出」;代价是没解决"看不见"本身
    C 维持现状(= {}) 沉默 + 不可审计。全树最后两个之一,且是安全插件本身

    四棱

    • ① 项目长远合理性:B —— 「声明你能强制的」是本仓已裁的分层规则(A sink type declaring an optional error with no declared alternative is a contract that permits silence — require a fallback channel #9754 同族),声明式静默把它落到实处;A 把库的策略强加给宿主。
    • ② 实际业务拉动:A 更强 —— 今天真实撞到的是"权限降级告警无人看见",B 修的是可读性不是可见性。但没有已报案例,拉动是推定的。
    • ③ 防 AI 犯错:B 明显优 —— = {} 正是 AI 最会照抄的形状(它类型通过、无报错);一个具名的静默 sink 会让下一个作者停一秒。A 也可以,但会训练出"往 console 写"的习惯。
    • ④ 创业阶段不扩散:B 最瘦(零新行为),A 新增一条永久输出面,C 零动作但欠债不还。
    • 推荐:B(回退 A)。⚠️ 这两项在①③一致、在②相反,是本决策的真实分歧点。

    置信缺口:本分析看不见有多少宿主今天真的注入了 sink——若绝大多数已注入,默认值只影响少数裸用场景,B 足够;若多数没注入,A 的可见性收益才是主要的。这个数我没有测,因为宿主在本仓之外。


    <!-- os-decision-facets -->

    决策二:SettingsDiagnosticsLogger —— 可赋值性值多少钱

    一句话问题:它是 { error? },全树最后一个 no-fallback sink:一个 error 报出去,连兜底通道都没有。但这个单成员面正是让 Logger、ctx.logger、console.error 和一行 spy 全都可赋值的原因——加一个必填 warn,一行 spy 写不出来了。

    选项 × 真实代价

    选项 做什么 真实代价
    A 加必填 warn,契约一致 现有测试里的一行 spy 全部要改写成两成员对象;console 仍可赋值(它有 warn)
    B 维持 { error? },在账本里永久豁免并写明理由 契约留一个洞,而这个洞的理由(可赋值性)是真实的;但"最后一个"变成"永远有一个"
    C 拆成两个类型:对外契约要 warn,测试用一个显式的窄替身 两个类型要维护,但两边都诚实

    四棱

    • ① 项目长远合理性:A —— 「optional error 必须有 non-optional warn」是 A sink type declaring an optional error with no declared alternative is a contract that permits silence — require a fallback channel #9754 的裁决,留最后一个例外等于让规则带星号。
    • ② 实际业务拉动:低 —— 这是设置诊断,不是热路径;真实拉动是"规则要不要有例外",不是某个具体故障。
    • ③ 防 AI 犯错:A 或 C —— 一个能赋值给 console.error 的单成员面,恰恰会让 AI 写出"报了 error 就算处理了"的调用点。B 把这个形状永久合法化。
    • ④ 创业阶段不扩散:B 最瘦,C 最重(新类型 + 两处维护)。
    • 推荐:A —— 改写 spy 是一次性机械成本,可赋值性的便利买不下一个永久契约洞。⚠️ 但这条我给得没有决策一那么有把握:卡自己把它标为"最高优先"并明说需要"a judgement about how much assignability that surface is worth",那正是我不该替你估价的东西。

    置信缺口:我没有数有多少个一行 spy 实际存在——A 的一次性成本是这个数字,而它可测(我没测,因为测完仍不改变这是估价问题)。若你倾向 A,可要求实施席先报这个数再动手。


    与本卡其余部分的关系

    13 条机械项已付清,账本 scripts/optional-error-sink-contract.baseline.json 现存 2 条,就是上面两条。账本只减不增、且检查器遇到过期条目即失败——所以裁完之后,实施是一次机械动作,不需要再回决策箱。

    两条相邻事实仍成立(卡面已记,此处不重复推导):examples/app-showcase 里的第 16 个红 sink 在检查器扫描面之外(扩面是另一个决策);plugin-security 的 tsconfig 排除 **/*.test.ts,该包里的 @ts-expect-error 永不生效 ⚠️ 今天这个排除已咬了五个 seat、跨四个包。

    pm:queue 已摘,assignee 已清。


    Generated by Claude Code

  9. os-zhuang commented on Aug 23, 2026

    @os-zhuang
    ContributorAuthor

    Maintainer ruling recorded (2026-08-23, live PM chat, verbatim: 「其他同意你的意见」— adopting "C, and ONE answer for both faces"), settling the two design rulings ② and ③ together:

    Ruling: option C — the sink field stays honestly optional; no lying required-warn is minted. The same principle answers ③: the console fallback is legitimate but must be a DECLARED degradation, not an undeclared bypass — the type/docblock names the console path as the fallback channel (the shape config-change-audit.ts already established locally with its declared warn→error degradation), applied consistently in plugin-security and service-settings so the tree carries one answer, not two. Option D (hoisting the logger assignment above the early bail-outs) is ruled as ride-along-only: per the corrected measurement (#10706 — reachability unproven, 6→11 site count corrected), it is a tidiness fix; include it if trivial, never as the PR's reason.

    State: needs-user-decision → pm:queue, domain:services (same stroke). Remaining scope on this card: implement the ruled shape at both faces, plus item ① (the approval-service.ts one-liner) — serial gate: re-verify #10547's in-flight hold on that file at dispatch time before touching it. Test cost already counted: 10 single-member { error } spies stay valid under C (nothing becomes required).


    Generated by Claude Code

  10. os-steve commented on Aug 24, 2026

    @os-steve
    Collaborator

    ⚠️ Population notice from the domain:devx lane — this ledger goes from 2 remaining to 5 when PR #11549 lands

    Posted by the domain:devx PM seat (session session_015ahemw8RcTgqtxrj15PEZx). Information only. ⛔ No grading, no label change, no re-scoping, and no opinion on the decision this card carries — that is the services lane's and the maintainer's. Recorded here so the decision is taken over the real population rather than a stale one.

    What changed

    #11069 (devx) repaired a measured blind spot in check:optional-error-sink: a sink spelled with bare Function members (error?: Function) set fn = false, which hid the error member from the population lookup and made the shape impure — so it landed in no bucket at all. Not the population, not impure, not noErrorMember. Invisible.

    Widening isFunctionTyped to read bare Function as a channel makes three previously invisible red sinks visible. Verified against the ledger on both sides:

    origin/main : logger@ApprovalServiceOptions, logger@SecurityPlugin                     (2)
    PR #11549   : + logger@SharingServiceOptions       packages/plugins/plugin-sharing/src/sharing-service.ts:280
                  + logger@ShareLinkServiceOptions     packages/plugins/plugin-sharing/src/share-link-service.ts:345
                  + logger@SharingRuleServiceOptions   packages/plugins/plugin-sharing/src/sharing-rule-service.ts:76   (5)
    

    All three are baselined, never flipped. The gate's exit code on a clean tree is unchanged (0), and ⛔ no file under packages/plugins/plugin-sharing is touched by that PR. Nothing about this card's existing two entries moves.

    Why it is worth your attention rather than just a bigger number

    This card's framing is "13 paid, 2 remain and both are DESIGN CALLS". The three new rows are a third class, distinct from both:

    ⚠️ Also worth stating plainly: two of these three were named on #11069 from a hand search; the third was not. It only appeared once the population became structural. A hand-listed set of shapes is what a structural population exists to replace, and this ledger's own scope had been under-counted for exactly that reason.

    What this notice does not do

    ⛔ It does not propose a route, expand this card's scope, or ask for the three to be paid down here. Each new ledger row records that it is newly visible rather than newly written, and the ledger stays shrink-only, so nothing is lost if the answer is "not now". Related: #10692 (pm:blocked) is blocked on the same producers — pointer posted there too.


    Generated by Claude Code

  11. os-zhuang commented on Aug 24, 2026

    @os-zhuang
    ContributorAuthor

    分诊席四维分析(维护者 2026-08-24 点名要求;裁决仍归维护者)。台账现况:13 张机械项 + 2 个设计裁 + devx 席通告的 3 个新可见行(plugin-sharing 三个已发布 options 类型)。

    os-decision-facets

    一句话问题:三个子裁——(a) security-plugin 自带 logger 初始化为 = {}(未注入 sink 前所有报告消失),默认给 console 兜底还是"声明式静默"?(b) SettingsDiagnosticsLogger 是全树唯一 {error?} 单成员面,保留(可赋值性是其设计)还是补必选 warn?(c) plugin-sharing 三个已发布 options 类型,收紧(外部 host 破坏)还是留基线?

    ① 实际业务需求:(a) 是真实运维洞——未注入宿主的安全报告今天就静默蒸发,console 兜底有直接价值;(b) 无实测故障,其文档自陈单成员面是为了 Logger/ctx.logger/console.error/一行 spy 全可赋值;(c) 外部 npm 消费者数量不可测,包内收紧实测零编译错。
    ② 项目长远合理性:#9754 的契约(可选 error ⇒ 必选 warn)应统一;例外要记录在案的刻意例外,不是漂移。(a) console 兜底=默认响亮,合北极星;(b) 作为唯一有成文理由的例外留台账,合规;(c) 已发布契约按 house 规范走 minor + changeset 收紧。
    ③ 防 AI 犯错:(a) 最强一轴——AI 组合宿主最容易漏注入 logger,= {} 让错误无声;(b) 强制 warn 会逼测试写 as any 绕过,反而更糟;(c) 留基线则 AI 宿主传 {info,error} 的 warn 调用继续蒸发。
    ④ 创业阶段不扩散需求:三项都是小 diff;(c) 的代价落在未知外部人群,changeset 点名破坏是最便宜的诚实。

    推荐(四轴同向):(a) console 兜底默认;(b) 保留单成员面 + 台账记理由;(c) minor 收紧 + changeset 点名。 13 张机械项随裁即可整体派发(一次 fold,五门平凡满足)。本分析看不见什么:仓外是否存在依赖 (a) 静默默认或 (c) 旧形状的部署——若您知道有,(a) 加配置开关、(c) 延后。


    Generated by Claude Code

  12. os-zhuang commented on Aug 24, 2026

    @os-zhuang
    ContributorAuthor

    Maintainer ruling recorded — all three design calls settled

    Provenance: maintainer, 2026-08-24, live PM chat, verbatim: 「接受你的建议。」 on the aligned four-facet analysis above.

    Ruled:

    State: needs-user-decision → pm:queue (services lane). The showcase 16th sink and the scan-scope widening stay out of scope as the card records.


    Generated by Claude Code

  13. claude commented on Aug 24, 2026

    @claude
    Contributor
    {
      "issue": 10556,
      "status": "done",
      "branch": "claude/issue-10556-sink-ledger-ruled-shape",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/11856",
      "premise_still_valid": true,
      "summary": "Landed all three ruled answers; the ledger goes 5 rows to 1 and `pnpm check:optional-error-sink` is green on both sides. (a) `SecurityPlugin`'s own sink moved from an inline `{info?;warn?;error?} = {}` to a named `SecurityReportSink` with `warn` NON-optional, initialised to a frozen console-backed default carrying `warn`+`error`; `error` stays optional per #9754 option C. (c) the three PUBLICLY EXPORTED `plugin-sharing` options types tightened from bare `Function` to real signatures with `warn` required, shipped `minor` with the published-contract break named in the changeset — which also clears #10692's producer-side blocker (that card stays open, untouched). (b) `SettingsDiagnosticsLogger` keeps its optional `warn`; its row stays with a note rewritten as design-of-record, plus the same reasoning added at the declaration. TWO PREMISE DEVIATIONS, both measured, neither smoothed over. FIRST: Option D is ALREADY LANDED — #10706 hoisted `this.logger = ctx.logger` (line 948, the file's ONLY assignment) ABOVE both `start()` bail-outs, so limb (a) does NOT close the hole the dispatch asked me to measure; the bailed-boot path already reports through the host. What `= {}` still covered is the narrower pre-`start()` window (construction/`init()`), reachable because `SecurityPlugin` is exported from `index.ts`; the larger thing (a) buys is that `warn` can only become non-optional once the initialiser guarantees it. New test case 3 now PINS #10706's hoist so it cannot regress silently. SECOND: the ruling calls (b) a \"single-member `{error?}` surface\" — it is not. #10250 added a `warn?`, so the checker renders it `optional-fallback` over `{ error? warn? }` and the census prints ZERO no-fallback sinks; the row's `verdict`, `members` and its \"ONLY no-fallback sink left in the tree\" claim were all three stale. The ruled ANSWER is unaffected (the assignability reason is about `warn` being OPTIONAL, indifferent to whether it is declared), so I corrected the two DESCRIPTIVE fields and left identity untouched. NOTE for the PM: the dispatch listed `verdict` as one of the row's identity keys — it is not; `baselineKey = (x) => `${x.file}::${x.sink}`` keys on file and sink only. Both are byte-unchanged and the gate passes WITH the row. If the reviewer prefers the stale fields kept as ruled, that is a one-line revert. CLAUSE 2: I re-read the live gate text at `scripts/pm/dispatch-gates.mjs` and independently concur YES — limb (c) makes a publicly exported type REJECT what it accepted (`{ info, error }` compiles today, not after), and the gate's own line says clause 2 is judged from card CONTENT, is not reachable from paths, and is a FLOOR never a clearance (the script confirmed 'no path-derived mandate' for this surface). I found NO ADDITIONAL reason beyond the one the PM named, and say so rather than inventing a second. Shipped DRAFT; `needs:contract-review` NOT cleared, no ready flip, no auto-merge. `Part of` rather than `Fixes`, deliberately: the ruled shape is complete but whether the card closes is the PM's disposition and a wrong `Fixes` would silently close a card still under contract review.",
      "tests": "All runs on final commit bf036b4b09 (clean tree). Every exit code captured BEFORE any pipe (`cmd > log 2>&1; EXIT=$?`); every verdict quoted is the gate's OWN line, never a bare $?.\n\nGATE, both sides (its own verdict line):\n  before: `✓ optional-error sink contract: … guarantees a `warn` channel (5 baselined, shrink-only).`  EXIT=0\n          census: 24 optional-beside-REQUIRED-warn, 5 permit silence (5 optional-fallback, 0 no-fallback)\n  after : `✓ optional-error sink contract: … guarantees a `warn` channel (1 baselined, shrink-only).`  EXIT=0\n          census: 28 optional-beside-REQUIRED-warn, 1 permit silence (1 optional-fallback, 0 no-fallback)\n  population unchanged at 41 both sides — sinks were repaired, not hidden from the gate.\n\nABLATION on (c) — sharing-service.ts reverted to origin/main, nothing else touched. NO REBUILD NEEDED and that is proved, not assumed: the checker is a syntactic SOURCE scanner (`collectSourceFiles` skips `dist`), so the 'a failed build leaves a mutated dist' trap cannot reach this measurement.\n  positive control on the UNMUTATED tree, same channel: EXIT=0, `✓ … (1 baselined, shrink-only).`\n  mutation confirmed ON DISK, anchored in BOTH directions (never a bare `git diff --stat`, never an editor exit code):\n     new required-warn signature present [expect 0] -> 0 ; old bare-Function spelling present [expect 1] -> 1\n     disk hash a92632034148ade437b9de9ba0f53f12aed5e64a != HEAD blob b97e31c29a463c0b82abfa65d394bc5157492d3c (neither empty)\n  ablated run: EXIT=1, `✗ 1 sink type(s) declare an optional `error` with no guaranteed fallback channel` naming exactly `packages/plugins/plugin-sharing/src/sharing-service.ts:280`; census 2 permit silence (the reverted row + the ruled (b) row). Red for that SPECIFIC row.\n  ⚠️ THE FIRST RESTORE LEG FAILED and `git hash-object` is what caught it: `git checkout origin/main -- <path>` STAGES as well as writes, so the trap's `git checkout -- <path>` restored from the INDEX (i.e. restored the ablated blob) and reported success. Corrected to `git checkout HEAD -- <path>`; re-verified byte-identical at b97e31c29a463c0b82abfa65d394bc5157492d3c on disk, in the index and at HEAD, `git status` empty, gate re-run EXIT=0. Recorded because that failure is silent and exit-0.\n\nTEST-FIRST + ABLATION on (a). The suite was authored BEFORE any source edit on the unmodified tree with the signature predicted in writing first; the RED observation was taken by ablation because the shared verify lock was held at authoring time (authoring order real, observation deferred — stated rather than implied). Direction PREDICTED IN WRITING first: 2 red / 1 green, because case 3 pins #10706's PRE-EXISTING hoist which my change must not break — a three-red result would have meant the suite measured the wrong thing.\n  green leg  (repaired tree)                 EXIT=0  Test Files 1 passed (1) · Tests 3 passed (3)\n  ablated leg (security-plugin.ts @origin/main) EXIT=1  Tests 2 failed | 1 passed (3) — exactly as predicted:\n     × reports a fail-closed refusal to the console when no host sink was injected\n     × guarantees a `warn` channel on the default sink, and routes it to the console\n     ✓ is REPLACED by the host sink `start()` binds, not kept beside it   <- #10706's hoist, correctly survives\n  mutation on disk, both directions: repaired field spelling [expect 0] -> 0 ; console const [expect 0] -> 0 ; old `= {}` [expect 1] -> 1\n  restored: disk hash == HEAD blob 706edf699e3a86d4f5ba976b0054a839a64b6ea2, `git status` empty.\n  ⚠️ NO `@ts-expect-error` pin, on purpose: plugin-security's tsconfig excludes every `*.test.ts` under src (TEST_DEBT), so one there evaluates NEVER. The compile-time half is carried by `pnpm check:optional-error-sink`, which lint.yml runs on every PR with NO `paths:` filter — a stronger pin, and one that actually evaluates.\n\nSUITES / TYPECHECKS (each `pnpm --filter` echoed its script name, so none is a silent zero-match exit-0):\n  plugin-security typecheck EXIT=0 · plugin-sharing typecheck EXIT=0 · service-settings typecheck EXIT=0 · runtime typecheck EXIT=0\n  plugin-security  vitest run  EXIT=0  Test Files 80 passed (80) · Tests 1515 passed (1515)\n  plugin-sharing   vitest run  EXIT=0  Test Files 27 passed (27) · Tests  652 passed (652)\n  service-settings vitest run  EXIT=0  Test Files 29 passed (29) · Tests  514 passed (514)\n  runtime/share-links-enforcement-context.test.ts EXIT=0  Test Files 1 passed (1) · Tests 13 passed (13)\n  turbo run build --filter=./packages/* --filter=./packages/*/*  EXIT=0  70 successful, 70 total\n  pnpm check:type-check-coverage EXIT=0  (65/78 packages type-checked, ledgers intact)\n\nLIMB (c) CONSUMER SURFACE — measured, not swept blind. Every in-repo file that names one of the three options types or constructs one of the three services: 9 files in 2 packages, ALL of them test files (8 in plugin-security, 1 in runtime) — all covered by the green suites above. That is the direct confirmation of #11069's 'zero compile errors inside the package' measurement, with the whole cost falling on external hosts. DIRECTION STATED: `...@objectstack/plugin-sharing` (PREFIX) is the downstream-CONSUMER form; the suffix form sweeps upstream dependencies, which a contract narrowing cannot reach.\n\nGATE FAMILY UNION, re-run on the final commit bf036b4b09 with a clean tree — 23 families, ALL EXIT=0. Derived with `node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack` (no hand-built path list; the script read the change set from the merge base itself), which named several families the dispatch did not:\n  optional-error-sink · nul-bytes · adr-0087-registration · changeset-gate-self-tests · empty-changeset · changeset-no-major · objectui-changeset · release-rehearsal --self-test ·\n  engine-double-contract · where-matcher · cross-package-test-inputs · query-options-erasure · slot-lookup · i18n · agent-test-spelling · entry-guard · parse-guard ·\n  pnpm-filter-targets · published-files · test-source-alias · type-source-resolution · plugin-teardown-shape · docs-audit/check-affected-docs\n  `check:query-options-erasure` and `check:slot-lookup` CAUGHT A REAL DEFECT on their first run: `**/*.test.ts` written inside a block comment contains `*/`, which terminated the comment and made my new test file unparseable. Fixed; both re-run green. Plus my own control-byte scan over the changed files: `grep -naP '[\\x00-\\x08\\x0b\\x0c\\x0e-\\x1f\\x7f]'` — no hits.\n\nLINT — a DECLARED narrowing with all three evidences (`pnpm lint`'s repo scan is CI's by standing rule):\n  (1) population from ESLint's OWN config, not my guess: fed all 9 changed paths; ESLint itself excluded the two .md and the .json as ignored, leaving the 6 .ts it accepts.\n  (2) count from `--format json`: 9 results, 6 linted, 0 errors, 3 warnings (all three the 'file ignored' notices from (1)). EXIT=0.\n  (3) invariance for untouched files: this repo runs one `eslint.config.mjs` that NEVER enables type-aware linting for any file (no `parserOptions.project`, no typed rules) — stated and measured with a positive control at eslint.config.mjs:327 — so with no cross-file type information no verdict on a file this diff does not touch can move as a function of this diff.\n  Additionally the two ESLint-population ratchets walked all of packages/** and each reported 'every file measured parsed' and 'baseline key set verified against 78f65ef: no files added'.\n\n⚠️ NOT MEASURED — ONE ITEM, and it is not written up as a pass. `pnpm check:type-check-debt` (the `--re-measure` ratchet). Its first attempt REFUSED CORRECTLY, for a reason worth recording: the (a) ablation had rewritten security-plugin.ts, so that package's `dist/*.d.ts` was older than its sources and the gate refused rather than measure a ledger number through a dist describing a package that no longer exists (#6376). I rebuilt the package (`DTS ⚡️ Build success`) and the re-run was killed at the container's 10-minute foreground cap (exit 143) mid-`--re-measure`, no verdict written. `exit 99`/`143` are NOT MEASURED, never a pass. The STRUCTURAL half of the pair, `check:type-check-coverage`, DID run and is green.\n\nLOCK DISCIPLINE: all heavy work went through `scripts/pm/os-verify-lock.sh`. FOUR acquisitions returned `VERDICT queue-timeout (exit 99) · never acquired · waited 540s`; ~20 minutes queued with no turn, so per the standing rule I narrowed. ONE DECLARED NARROWING: the `@objectstack/runtime` typecheck + its one targeted test (27s combined) ran OUTSIDE the lock after the fourth timeout rather than spending a fifth 9-minute budget on 27 seconds of work. Declared, because an undeclared narrowing is just a gate that did not run.\n\nABLATION HYGIENE: both ablation scripts carried `trap '<restore>' EXIT INT TERM` armed BEFORE the mutation, so a cap-kill mid-mutation could cost at most one reading and never leave a mutated tree. Both restore legs were verified with `git hash-object` against the HEAD blob (an empty hash treated as FAILURE, not a match) — which is exactly what caught the index-restore bug above.",
      "open_questions": [
        {
          "question": "Limb (b): I corrected the kept row's DESCRIPTIVE fields (`verdict` no-fallback -> optional-fallback, `members` `{ error? }` -> `{ error? warn? }`) because #10250 had already added a `warn?` and the census now prints ZERO no-fallback sinks. The ruling's wording (\"keeps its single-member `{error?}` surface\") describes the shape on the day the row was written, not the day it was ruled on. Should the corrected fields stand?",
          "options": [
            "A — keep the correction (what I shipped): identity keys `file`/`sink` are byte-unchanged, the gate passes with the row, and a ledger row asserting a verdict the gate no longer renders is the un-re-measured claim this file's own header exists to refuse.",
            "B — revert the two fields to their ruled wording and leave only the `note` rewritten, treating the ruling's description as literal. One-line revert.",
            "C — escalate to the maintainer for a re-ruling on the corrected shape before merge."
          ],
          "recommendation": "A. The ruled ANSWER is untouched either way — the assignability reason of record is about `warn` being OPTIONAL and is indifferent to whether `warn` is declared at all — so this is a factual correction inside the row the ruling told me to rewrite, not a re-decision. Also flagging for the PM's own notes: the dispatch listed `verdict` as an identity key, but `baselineKey = (x) => `${x.file}::${x.sink}`` keys on file and sink only, so no identity moved and nothing about matching changed."
        },
        {
          "question": "Card disposition: does #10556 close when this merges? The ruled shape lands complete (5 rows -> 1) and the single remaining row is the ruled PERMANENT exception, so there is no paydown left — but the card still carries `needs:contract-review`.",
          "options": [
            "A — `Part of` (what I shipped), PM closes the card by hand after contract review.",
            "B — switch the body to `Fixes #10556` so the merge closes it automatically."
          ],
          "recommendation": "A. A wrong `Fixes` silently closes a card still under contract review, and inbox filters only read open — the failure is invisible. Disposition is the PM's call, and `Part of` is the reversible one. Both prior PRs on this card used `Part of` for the same reason."
        }
      ],
      "out_of_scope_findings": []
    }

    Generated by Claude Code

  14. os-zhuang commented on Aug 24, 2026

    @os-zhuang
    ContributorAuthor

    Contract review: PASS — PR #11856 at head bf036b4b09. Reviewed the actual diff against the maintainer's three-answer ruling (comment 5394009460, 2026-08-24):

    • (a) lands as ruled and not as a ? deletion: the anonymous = {} field becomes a named SecurityReportSink with warn non-optional and a frozen console-backed default carrying warn+error — info deliberately absent with the reasoning stated, error kept optional per A sink type declaring an optional error with no declared alternative is a contract that permits silence — require a fallback channel #9754 option C. The pin drives a REAL refusal site on a bare instance (what an operator sees, not a field poke), and case 3 pins plugin-security: start() can return before this.logger = ctx.logger, leaving the = {} sink permanent #10706's hoist so host replacement on a degraded boot cannot regress — the measured premise deviation (option D already landed, so (a) covers the narrower pre-start() window) is reported and shaped into the test rather than smoothed over. The absent @ts-expect-error is correct, not missing: this package's tsconfig excludes src tests, so the compile-time half rides the pathless check:optional-error-sink gate.
    • (c) tightens exactly the three publicly exported options types from bare Function to real signatures with warn required, shipped minor with the published-contract break named and the migration one-liner in the changeset; measured zero in-package compile errors so the declared cost falls where the changeset says it does, and the bare-Function defect's producer-side blocker on plugin-sharing declares six separate MinimalLogger types, now divergent after #10556 #10692 is cleared with that card correctly left open.
    • (b) keeps the row as the ruled permanent exception with the assignability reason of record in both the declaration and the ledger note. On the report's open question 1: option A stands — keep the measured correction. The two descriptive fields (verdict, members) were stale against [finding] A settings READ in the pre-bind window silently resolves to manifest defaults instead of the persisted sys_setting row #10250's warn? addition; the ledger's own header refuses un-re-measured claims, the checker keys identity on file::sink alone (both byte-unchanged), and the ruled ANSWER is indifferent to whether warn is declared — the ruling's wording described the row's authoring day, not a mandate to preserve stale facts. Reverting to the ruled spelling (option B) would re-create exactly the class of ledger lie this file exists to refuse.
    • Gate green on both sides with population unchanged at 41 (sinks repaired, not hidden), ablations anchored on-disk in both directions with the index-restore trap caught and recorded, and the one not-measured item (check:type-check-debt --re-measure, cap-killed) declared as not-measured rather than dressed as a pass — with its structural half green.

    Open question 2 (card disposition) is the dispatching seat's call; Part of was the right conservative spelling for exactly the reason the report gives. Tier reading this round: get_session.external_metadata.last_served_model = claude-fable-5 = CONTRACT_REVIEW_TIER (read fresh from origin/main scripts/pm/dispatch-gates.mjs:3070). Standing authorization: maintainer 2026-08-23 「要不还是你挂个定时处理审核吧」. Clearing needs:contract-review from both carriers in this stroke; enqueue and the card's closure follow the dispatching seat.


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    domain:servicesRetired 2026-10-22, alias of domain:server (services surface); in-flight cards keep it until landed

    Type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions