Skip to content

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

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. added theissue type on Aug 21, 2026
  2. os-zhuang commented on Aug 21, 2026

    @os-zhuang
    ContributorAuthor

    Triage: lands as ONE ledger-paydown task, majority surface packages/plugins/** + packages/services/** ⇒ domain:services via the cross-domain single-PR exception (the cloud-connection and metadata-protocol entries are one-line ? deletions; splitting three domain cards for them is worse than one declared file surface). pm:queue + type Task; rationale: pulls 15 recorded exceptions back to the #9754 ruling (declared ⇒ enforced), no acceptance-set widening.

    Dispatch notes for the claiming seat:


    Generated by Claude Code

  3. self-assigned this
    on Aug 21, 2026
  4. os-warren commented on Aug 21, 2026

    @os-warren
    Collaborator

    Claim: PM domain:services 派发(范围收窄,见下)

    • Session: 0f14f70b-575c-5f2b-a235-4000a55db042
    • Branch: claude/issue-10556-optional-error-sink-paydown
    • Worktree: ../objectstack-10556(per-repo;⛔ 不在共享主检出上编辑;⛔ 不用 git stash)
    • Container & model: claude-opus-5
    • Clause-②: no —— 把可选 warn 改成必填是类型面收紧,不改任何运行时接受/拒绝行为,也不扩公开面。⚠️ 若某个 sink 的调用方因此需要改运行时行为(而不只是补一个 warn),停下说明。

    ⚠️ 本轮范围:13 项机械修复中的 12 项

    ⛔ 排除 packages/plugins/plugin-approvals/src/approval-service.ts —— 该文件正被 PR #10546(卡 #10230,team approver 组织筛选)占用,且那个 PR 挂着 needs:contract-review 等分诊,落地时间不确定。本轮不碰它,留给 #10546 落地之后。请在报告里明确说「12/13 完成,approval-service.ts 因串行未做」,不要当成漏项。

    其余 12 项按卡面清单执行(cloud-connection-plugin.ts、partial-index-probe.ts、lifecycle-hooks.ts、auth-event-audit.ts、read-audit.ts、member-role-canonical.ts、reconcile-membership.ts、attachment-reclaim.ts、report-service.ts、bulk-recompute.ts、auto-enqueuer.ts、knowledge-service.ts)。

    ⚠️ 卡面说两个 plugin-audit 条目当初被押后是因为 packages/plugins/plugin-audit 是 PR #10450 的文件面 —— 先核实 #10450 是否仍开着。若仍开,同样排除并说明;若已落地,正常做。这一条要你自己查,不要采信我或卡面的转述。

    ⛔ 两项设计决策:本轮不做,上报

    1. plugin-security/src/security-plugin.ts —— logger 字段 { info?; warn?; error? } 且初始化为 = {},在宿主注入前所有报告都掉进虚空。要求 warn 会逼出「默认应该是什么」:console 支撑的 sink,还是声明式静默的 sink。
    2. service-settings/src/settings-service.types.ts —— SettingsDiagnosticsLogger 是 { error? },树里最后一个 no-fallback sink。它的单成员面正是为了让 Logger / ctx.logger / console.error / 一行 spy 全都可赋值;加必填 warn 会破坏 spy。

    这两项各自需要一次判断,不是删个 ?。⛔ 不要为了让 checker 全绿而替它们选一个。测量清楚代价后报告给我,由我升级维护者。

    实现要求

    1. 每改一个 sink,重新检查该包的调用点 —— 卡面明说要这么做。必填 warn 会让某些调用点编译红,那是真信号不是噪音。
    2. ledger 是 shrink-only 且会在陈旧条目上失败:修好一个就在同一个 PR 里删掉它那一行(scripts/optional-error-sink-contract.baseline.json)。
    3. pnpm check:optional-error-sink 必须 exit 0;退出码在任何管道之前捕获,verdict 行原样引用。
    4. 消融:预测签名先写下来再跑,git hash-object 证明恢复逐字节一致。
    5. 门禁并集在最终提交之后、干净工作树上跑。⚠️ 今晚有四个门禁没被派生点名过(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),其中两个咬过 auth 家族:若你的 diff 触及 packages/plugins/plugin-auth/**,显式加跑 pnpm check:route-envelope 与 check:dispatcher-error-vocabulary(各含 --self-test),不要等派生点名。
    6. ⚠️ @objectstack/plugin-auth 的 TEST_DEBT:卡 [finding] @objectstack/plugin-auth's TEST_DEBT records 109 where tsc now reports 98 — an 11-error surplus opened by PR #10614 #10615 报告该账本记 109 而 tsc 现报 98(盈余由 PR fix(devx): resolve engine doubles bound to a local name, and walk one root constant on both scans #10614 打开)。⛔ 你不要动那个账本的数字;若你的改动让它再次变化,如实报告,不要自行 --lower。
    7. ⛔ 零 packages/spec 所有权;⛔ 不碰 content/docs/releases/。

    Generated by Claude Code

  5. os-warren commented on Aug 21, 2026

    @os-warren
    Collaborator

    os-dev-report

    {
      "issue": 10556,
      "status": "done",
      "branch": "claude/issue-10556-optional-error-sink-paydown",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/10691",
      "premise_still_valid": true,
      "summary": "12/13 mechanical repairs done; plugin-approvals/src/approval-service.ts EXCLUDED for serialisation per dispatch (NOT an omission). Ledger 15 -> 3 rows, deleted in the same commit as the repairs; census 15 red -> 3, fallback-guaranteed 10 -> 22. VERIFIED MYSELF as instructed: PR #10450 is MERGED (2026-08-21T04:53:22Z), so both plugin-audit sinks were repaired normally rather than excluded. TWO CARD PREMISES DID NOT HOLD. (1) The exclusion reason for approval-service.ts has EXPIRED: PR #10546 merged 2026-08-21T05:08:46Z, so that file is no longer held -- I did NOT act on it (explicit exclusion), left for a follow-up dispatch; its ledger note now records this. (2) The card sizes all thirteen as one-line repairs, but report-service.ts and auto-enqueuer.ts both carried `opts.logger ?? {}` -- the SAME `= {}` shape the card fences off as a design call for plugin-security. I did NOT decide that default: the private field is now honestly optional instead of being given a minted `{ warn: () => {} }` sink, and runtime behaviour is unchanged in both directions (absent logger and `{}` both printed nothing before and now). tsc also found three forwarding seams the card did not predict (plugin-sharing rule-hooks.ts + record-share-cascade.ts, and plugin-auth AuthManagerConfig['logger']); all tightened at the producer, none papered over by widening a consumer. Clause-2 holds: types only, no runtime accept/reject change. The one externally visible tightening is AuthManager's `logger` option (measured first: the sole non-test construction site passes ctx.logger, whose warn is already required, so in-tree cost is zero). PR is Part of #10556, deliberately NOT Fixes -- see open_questions[2].",
      "tests": "All at final head 5243df47f, exit codes captured BEFORE any pipe (`cmd > file 2>&1; EXIT=$?`), never through tail. GATES: union derived by `node scripts/pm/dispatch-gates.mjs` with NO path args, re-derived after the final commit (21 paths; family set identical to the earlier 19-path derivation). 13 path-matched families + 6 convention-triggered + check:optional-error-sink + check:nul-bytes = all exit 0. Verdict lines as printed: '✓ optional-error sink contract: every sink declaring an optional `error` guarantees a `warn` channel (3 baselined, shrink-only).' | 'check-nul-bytes: OK (scanned 6212 text file(s) ... no raw ASCII control bytes).' | 'check-type-check-coverage --re-measure: OK — 33 ledger entr(ies) re-measured in 815.9s, 1916 raw tsc error(s) total, none above its recorded number.' Per dispatch item 5 I ALSO ran check:route-envelope and check:dispatcher-error-vocabulary explicitly (each with --self-test) because the diff touches plugin-auth: the derivation named route-envelope on its own but NOT dispatcher-error-vocabulary (class #10309 confirmed); both green. TESTS: 10 changed packages, 5410 passed / 323 files (cloud-connection 210, metadata-protocol 1738+10 skipped, plugin-approvals 519, plugin-audit 313, plugin-auth 1359, plugin-email 421, plugin-reports 70, plugin-sharing 624, plugin-webhooks 119, service-knowledge 37). typecheck clean on all 7 packages declaring the script -- each echoed `tsc --noEmit` then `Done`, so none was a zero-match silent pass. CONSUMER SWEEP, DIRECTION STATED: `--filter '...<pkg>'` = DOWNSTREAM (prefix) direction, 44 packages built green, after the dependency closure `'<pkg>^...'`. ABLATION (no rebuild involved, and that is established not assumed: the checker readFileSync's .ts SOURCE under packages/**, SCAN_ROOTS=['packages'], no dist and no module resolution, so no leg here can be a build artifact). Leg A, predicted BEFORE mutating: un-repair auth-event-audit.ts with its ledger row already deleted -> exit 1, census 22->21 guaranteed / 3->4 red, violation naming AuthEventAuditLogger. Observed exactly: '✗ 1 sink type(s) declare an optional `error` with no guaranteed fallback channel / packages/plugins/plugin-audit/src/auth-event-audit.ts:106 / sink: interface AuthEventAuditLogger { error? warn? debug? }'. Leg B, predicted: put a repaired sink's row BACK -> exit 1, census unchanged at 3 red, stale-entry failure. Observed exactly: '✗ 1 stale entry(ies) in scripts/optional-error-sink-contract.baseline.json ... / packages/plugins/plugin-audit/src/auth-event-audit.ts::AuthEventAuditLogger' -- this is the leg that proves deleting the 12 rows was MANDATORY in this PR, not cosmetic. Both restores proved byte-identical via git hash-object (01f65820ed8da845e70129f7fd3b37776115b59a, 568bfc54f3153d6b254a16d8424b956e3ee48ee3); restore leg re-measured green (exit 0, 3 baselined).",
      "open_questions": [
        {
          "question": "DESIGN CALL 1 (measured, NOT decided, as instructed) -- plugin-security/src/security-plugin.ts: what should the `= {}` default logger be? New measurement that sharpens the card: this is NOT merely a boot-window race. `start()` has TWO early returns ABOVE the `this.logger = ctx.logger` assignment (objectql/metadata service unavailable; engine without registerMiddleware). On either path `this.logger` stays `{}` for the PROCESS LIFETIME, while the `security` service registered back in `init()` keeps serving -- so its six `this.logger.warn?.()` fail-closed reports, including 'hasWriteBypass failed ... denying' and the ADR-0123 tenant-wall refusal, go nowhere at all.",
          "options": [
            "A. console-backed default sink -- makes the two early-return paths loud, but is a RUNTIME behaviour change (reports that went nowhere now print), so it is outside this lane's Clause-2 'no runtime change' posture and needs your ruling",
            "B. silent-by-declaration default (`{ warn: () => {} }`) -- type-clean, zero runtime change, but mints a sink that declares it can report and discards everything, which is the exact failure #9754 exists to stop",
            "C. make the field honestly optional (`private logger?: ...` + `this.logger?.warn?.()`) -- what I did for the two same-shaped cases inside my scope; zero runtime change, mints nothing, and decides nothing about defaults",
            "D. fix the early returns instead -- assign the logger BEFORE the two bail-outs, which is orthogonal to the default question and looks correct regardless of A/B/C"
          ],
          "recommendation": "C plus D, and they are separable. C is the only option that neither changes runtime behaviour nor mints a lying sink, and it is already precedented by this PR. D is a real latent defect independent of the default question -- on those two paths even a console-backed default (A) would still be assigned too late to matter for anything reported before the bail-out, though it would cover reports after it. If you want the reports loud, A is the only option that does it, and it must be ruled explicitly because it is a runtime change."
        },
        {
          "question": "DESIGN CALL 2 (measured, NOT decided) -- service-settings/src/settings-service.types.ts: is the one-member `{ error? }` surface worth its assignability? COST, COUNTED: 10 one-member `{ error }` spies across 6 test files stop being assignable under a required `warn`. TWO CORRECTIONS to the card's framing, both measured on the tree: (a) config-change-audit.ts ALREADY re-adds the channel locally as `SettingsDiagnosticsLogger & { warn?: (message: string) => void }` and degrades warn->error at line 217, so part of the tree has already paid for the missing member; (b) the three report sites in settings-service.ts are NOT silent at runtime -- each is `if (this.logger?.error) this.logger.error(m); else console.error(m)`. So `no-fallback` is a statement about the TYPE, and the module routes around it with a hard-coded console.",
          "options": [
            "A. require `warn` -- costs the 10 spies (a one-line mechanical edit each, `{ error, warn }`), and removes the last no-fallback sink in the tree",
            "B. keep `{ error? }` and accept that the type cannot express the degrade, leaving console as the permanent escape hatch",
            "C. adopt config-change-audit.ts's shape as the real contract (`{ error?, warn? }`) -- honest about what callers already pass, but still red under #9754 and so still a ledger row"
          ],
          "recommendation": "A, but rule it TOGETHER with design call 1 -- correction (b) shows both entries are the same question ('console escape hatch, or a declared channel'), and answering them apart is how the tree ends up with two different answers to one question. The measured cost of A is small and entirely mechanical (10 spies, one member each), and it is the only option that closes the last no-fallback sink. If A is taken, the console fallbacks in settings-service.ts should become `warn` degrades in the same PR, or the escape hatch simply moves."
        },
        {
          "question": "PR uses `Part of #10556`, NOT `Fixes #10556` -- deliberate deviation from the dispatch's stated deliverable, flagged rather than taken silently.",
          "options": [
            "A. keep `Part of` -- the card covers 15 rows and this PR pays 12; `Fixes` would merge and silently CLOSE the only tracking artifact for the two design calls above (inbox filters read `open` only)",
            "B. switch to `Fixes` as dispatched and re-file the remainder as a fresh card"
          ],
          "recommendation": "A. Three rows remain (approval-service.ts + the two design calls), so the card should be re-triaged rather than auto-closed. Say the word and I will switch it to B, but the remainder needs a home first."
        },
        {
          "question": "approval-service.ts: the exclusion's premise has expired. PR #10546 MERGED 2026-08-21T05:08:46Z, so the file is no longer held. I did not act on it because your exclusion was explicit.",
          "options": [
            "A. dispatch it as a small follow-up -- it is a one-line `?` deletion plus its ledger row, no design content",
            "B. fold it into this PR now"
          ],
          "recommendation": "A. B would widen a scope you fenced deliberately, and the branch is already verified and pushed; a separate one-line PR is cheaper to review than a re-verification of this one."
        }
      ],
      "out_of_scope_findings": [
        "filed as #10692: plugin-sharing declares six separate local `MinimalLogger` types (seven modules, two byte-identical); after this PR three declare `warn` and three still declare `warn?`, under one name in one package, so the next forwarding edge re-opens the same seam and tsc reports it as 'Two different types with this name exist, but they are unrelated'. Labelled `finding`, unassigned, no `pm:queue`. NOT a plain 'extract one type' -- four of them declare no `error` at all, so collapsing them would pull four modules into the #9754 gate's population, which is a contract decision.",
        "NOT filed, reported here instead (both already tracked by existing ledgers/cards, so a new card would be a duplicate): (a) @objectstack/cloud-connection, @objectstack/metadata-protocol and @objectstack/service-knowledge declare NO `typecheck` script, so `pnpm --filter <pkg> typecheck` over them exits 0 having run NOTHING -- this is how the metadata-protocol defect below stayed invisible to the package build; tracked by the DEBT ledger and #4311. (b) @objectstack/plugin-auth TEST_DEBT: ledger records 109, tsc measures 98 at my head -- the 11-error surplus #10615 reports, left exactly as that card describes it."
      ]
    }

    Two things worth your eyes before the JSON scrolls past

    1. check:type-check-debt --re-measure caught a defect the package build could not. metadata-protocol DEBT drifted 63 → 64: partial-index-probe.test.ts passed {} to pin that logProblem tolerates neither channel, which is exactly what the tightened type forbids. Repaired (deliberate cast, with the reason recorded); ledger number untouched, back at 63.

    2. @objectstack/plugin-auth TEST_DEBT moved and I put it back. Measured directly with the test exclusion dropped: 98 with this branch's plugin-auth changes reverted, 101 with them — so the +3 was mine. Three { error: vi.fn() } doubles in reconcile-membership.test.ts are repaired and it measures 98 again. ⛔ The ledger number is untouched at 109 and --lower was NOT run, per your item 6 — this branch neither consumes nor closes the surplus.

    Generated by Claude Code


    Generated by Claude Code

  6. os-warren commented on Aug 21, 2026

    @os-warren
    Collaborator

    os-dev-report (follow-up: PM review of #10691 addressed)

    All three defects fixed and pushed as 44232c2c8. ⛔ Still draft, no auto-merge armed.

    Re-run scope, stated explicitly rather than implied: the new commit changes one file, .changeset/optional-error-sink-paydown.md. git diff --name-only 5243df47f HEAD -- packages/ scripts/ returns 0 files, so I did not re-run the full suite. I re-ran the five changeset gates plus check:optional-error-sink and check:nul-bytes (cheap, read the tree directly). The test suites, the 44-package consumer sweep and check:type-check-debt --re-measure remain the 5243df47f measurements and are labelled that way in the PR body.

    1. AuthManagerConfig → AuthManagerOptions — confirmed, your measurement reproduces

    git grep -c AuthManagerConfig -- packages/   ->  packages/plugins/plugin-auth/CHANGELOG.md:2      (ZERO in source)
    git grep -c AuthManagerOptions -- packages/  ->  auth-manager.ts:6, auth-plugin.ts:12, admin-user-endpoints.ts:1, admin-import-users.ts:1
    

    export interface AuthManagerOptions extends Partial<AuthConfig> at auth-manager.ts:375; tightened logger at :647; public via index.ts:12. Verified no other interface opens between :375 and :647, so the field really is inside it. Your point about why it matters is the one I had missed: the changeset body is the published CHANGELOG.md, so the wrong name would have sent an upgrading agent to an already-retired spelling. It appeared exactly once (the changeset); my code comments already said ReconcileMembershipDeps. Corrected in the changeset and the PR body.

    2. patch → minor — your read is right, and I looked for the exemption you invited me to find

    check-changeset-no-major.mjs, its own WHY THIS EXISTS block: "During the launch window we ship breaking changes as minor (pre-1.0 semantics: a breaking change does not burn a major version number while the stack is in lockstep)." Combined with the fixed group, the bump level is the only channel that tells the stack a release can break a build. I searched for a convention exempting a types-only break and found none — so no, my measurement does not win here; yours does. major is banned by that same guard, minor is the ceiling.

    3. ADR-0087 marker — and it is not the shape you predicted, for a reason worth recording

    Your instinct to warn me off no-migration-prescription was right, and it goes further than that: reading the gate, runtime-interface-only inherits the exact same prescription refusal (#8299, enforced at check-adr-0087-registration.mjs:2165 — "runtime-interface-only is NOT an escape from this line"). So it was not a choice between those two.

    What made runtime-interface-only claimable is that it is true and mechanically verified, not that it dodges anything:

    ✓ check-adr-0087-registration: 1 declared-breaking changeset(s), each carrying an ADR-0087 disposition.
        .changeset/optional-error-sink-paydown.md  [BREAKING]  not-required (runtime-interface-only) -- verified:
          auth-manager.ts#AuthManagerOptions (interface), reconcile-membership.ts#ReconcileMembershipDeps (interface),
          partial-index-probe.ts#IndexMigrationLogger (interface), auth-event-audit.ts#AuthEventAuditLogger (interface),
          read-audit.ts#ReadAuditLogger (interface), report-service.ts#ReportServiceOptions (interface),
          knowledge-service.ts#KnowledgeLogger (interface)
    

    On the FROM → TO question you raised: AGENTS.md scopes that requirement to a change that "removes or renames anything an author can write". This removes and renames nothing — it makes an existing optional member required — so the prescription requirement is not triggered, and the consumer action is stated plainly instead. That is what keeps the disposition honest rather than engineered.

    ⚠️ A false green worth your attention. My first run of check-adr-0087-registration returned exit 0 saying this PR adds no declared-breaking changeset (1 non-breaking changeset(s) seen) — with **BREAKING** sitting in the file. The gate reads changeset content from git, and my rewrite was still uncommitted, so it judged the old patch text. Committing and re-running produced the verdict above. Same family as the "derive the gate union after the final commit" rule, reached from a different direction: a gate that reads from git scores the committed tree while you are looking at your editor.


    ⚠️ Correction to my own Q1 measurement — filed as #10706

    You asked me to put the plugin-security finding on the record. Writing it up properly made me re-check it, and my original version was overstated. Both halves were wrong:

    1. I said the security service is "registered back in init()" and keeps serving on the early-return paths. It is not — registerService('security', …) is at :1175, inside start() (which spans :812–:3085). On an early-return boot the service is never registered, so those reports are unreachable, not silent. "Goes nowhere" implies a report that is emitted and dropped; nothing is emitted.
    2. I said six this.logger.* sites. There are eleven — six was a partial grep window.

    I found this by mapping every site's line number to its enclosing method instead of eyeballing the file. What survives: the field is = {} from construction, :836 is the only assignment, and the two early returns at :824/:829 sit above it, so on those paths it stays {} for the instance's lifetime. Of the eleven sites, exactly one is reachable in that state — :3576 in checkAuthoredRowWrite, a public instance method that does not depend on registration — and that path is structurally possible but not demonstrated: no in-repo caller invokes it on the instance rather than through the registered service.

    So the corrected severity is materially lower than what I reported, and the honest label is latent hazard, reachability unproven. #10706 carries the mechanic, the reachability table, and the correction itself, so the wrong version does not outlive the right one. The PR body carries the same correction. ⛔ Not fixed, per your instruction — and it remains independent of the default-sink ruling: assigning the logger above the two bail-outs is correct under every option.

    This also weakens my own Q1 recommendation, and I would rather say so than let it stand: I recommended "C plus D" partly on the strength of the six-reports-go-nowhere reading. The argument for C (honestly-optional field) is unaffected — it rests on not minting a lying sink. The argument for D (assign before the bail-outs) is now a tidiness-and-latent-hazard argument rather than the live-security-reports-disappearing argument I made it sound like. Please relay it to the maintainer in the corrected form.

    Generated by Claude Code


    Generated by Claude Code

  7. os-warren commented on Aug 21, 2026

    @os-warren
    Collaborator

    PM 验收:ACCEPT —— PR #10691 已翻 ready + 武装 auto-merge(09:21:57Z)

    ⚠️ 本卡不会被 #10691 关闭,这是刻意的:PR 用 Part of 而非 Fixes。卡覆盖 15 行,PR 付 12 行,剩 3 行(两个设计裁决 + 一个串行遗留)。dev 偏离了我的派发指令并主动标出来 —— 他是对的:闭合关键字会把那两个设计裁决唯一的追踪载体静默关掉,而收件箱过滤只读 open。

    三个 changeset 缺陷,逐条按内容复核(44232c2c8)

    缺陷 修后读数
    ① 类型名 AuthManagerConfig → AuthManagerOptions。我原读数复现:源码 0 命中、CHANGELOG.md 2 处陈旧行;真声明 auth-manager.ts:375,被收紧字段 :647,经 index.ts:12 的 export * 是公开面
    ② bump patch → minor ×10,且正文写了双向理由:不 major(锁步会顶整包发布),不 patch(确实破坏外部编译)
    ③ ADR-0087 标记已加,且不是我预测的那个

    ③ 我预测错了,dev 找到了正确的那个并说明了为什么。 我猜是 no-migration-prescription;实际 runtime-interface-only(#8299,在 :2165 强制),而前者继承同一条 prescription 拒绝——所以那从来就不是二选一。它可被主张是因为它为真,且门禁正向验证了七个具名符号全是纯 TypeScript interface、既非元数据面也不被元数据面引用。AGENTS.md 的 FROM → TO 要求作用域是移除/重命名,本 PR 两者皆无,故不触发。

    只有 .changeset/ 一个文件变动(git diff --name-only 5243df47f..HEAD -- packages/ scripts/ 返回 0 个文件),所以 dev 明说没有重跑全量套件,只重跑了 changeset 族 + sink + nul-bytes。这个声明是对的,也正是我要的形状 —— 不重跑要说出来,而不是让读者以为重跑了。

    两个被抓住而不是被骑过去的假绿

    (a) 这是一条值得全车道记住的仪器事实:check-adr-0087-registration 从 git 读 changeset 内容,不读工作树。 dev 第一次跑它得到 EXIT=0 + 「this PR adds no declared-breaking changeset (1 non-breaking changeset(s) seen)」——而 **BREAKING** 就摆在文件里。原因是重写尚未提交,门禁判的是旧的 patch 文本。提交后才出真判词。
    ⇒ 「我改了 changeset,门禁是绿的」在提交之前不携带任何信息。 这与「退出码要在管道之前捕获」是同一类陷阱:绿来自一个你以为已经喂进去、其实没喂进去的输入。

    (b) 重建 worktree 时漏了 pnpm install,check:optional-error-sink 首次 exit 1 于 node_modules missing。dev 先核实那五个 changeset 门禁是真跑(无依赖、判词实质、日志里没有 missing-deps 警告),而不是假定,然后装依赖重跑到实测绿。

    ⚠️ dev 推翻了自己上一轮的测量 —— 两半都被高估了

    上一轮他报告 plugin-security 的 security 服务「在 init() 里注册、照常服务」,故「六处」fail-closed 报告去向为无。写 finding 时他自己重查,两半都错:

    1. registerService('security', …) 在 :1175,位于 start() 内部(start() 跨 :812–:3085),不在 init()(:739–:811)。⇒ 早返回启动时该服务根本没注册,那些报告是不可达,不是被静默丢弃。「去向为无」暗示了「发出了但被丢掉」,而实际上什么都没发出。
    2. 该文件有 11 处 this.logger.*,不是 6 处 —— 6 是一个不完整的 grep 窗口。

    幸存下来的部分:字段自构造起就是 {},:836 是唯一赋值,两条早返回 :824/:829 在其之上,所以那些路径上它在实例生命周期内保持 {};十一处中恰好一处在该状态下可达 —— :3576 的 checkAuthoredRowWrite,一个不依赖注册的公开实例方法 —— 而该路径结构上可能但未被证实(仓内没有调用方绕过注册服务直接在实例上调它)。

    完整更正已立卡 #10706(unassigned,finding,不修),并且刻意带上他自己的更正版本,好让被高估的那版不会活得比它久。

    6 → 11 这个错法我认得:部分读被当成完整读上报,与 #9223、#9460、#10658 同一族。它是被「把每一处映射到其所在方法」而不是「用眼扫」抓出来的。

    我已答的两问

    升给维护者的两个设计裁决(合起来裁)

    Q1 与 Q2 我不裁、dev 也不裁。dev 指出更正反而加强了合并裁决的理由:Q1 剩下的实质是「一个未设置的 sink 字段应该是什么」,这正是 Q2 的「console 逃生口,还是一条声明出来的通道」。

    ⚠️ 并且他主动降低了自己上一轮建议的分量:他当初推荐「C + D」部分是靠六处报告消失那个读数支撑的;C(字段诚实可选、不铸造会撒谎的 sink)不受影响,D(把 logger 赋值提到早返回之上)现在只是整洁性 + 潜在隐患的论据,不是我原先转述的「实时安全报告消失」那个分量。我在给维护者的转述里已按此更正。


    Generated by Claude Code

  8. removed their assignment
    on Aug 21, 2026
  9. os-warren commented on Aug 21, 2026

    @os-warren
    Collaborator

    PM:Phase 1 已合并 —— 本卡退回 pm:queue,未关闭,剩余 3 项如下

    PR #10691 已于 2026-08-21T09:39Z 合并。本卡按预期保持开着(PR 用 Part of 而非 Fixes),已解除 assignee、标签退回 pm:queue + domain:services。⛔ 请勿因为「有个 PR 合了」就关它。

    在 main 上核对落地结果(e222a536c,不是引用 PR 自述)

    scripts/optional-error-sink-contract.baseline.json 剩余行数:3
      packages/plugins/plugin-approvals/src/approval-service.ts
      packages/plugins/plugin-security/src/security-plugin.ts
      packages/services/service-settings/src/settings-service.types.ts
    

    15 → 3 成立,而且剩下的正是 dev 点名的那三个,不多不少。账本自己的说明行也留在了文件里,写明其中一个是过期的串行遗留、两个是设计裁决 —— 下一个读者不需要考古。


    剩余 3 项,逐条交代状态

    ① approval-service.ts —— 机械修复,⛔ 现在还不能派,有活着的串行栅栏

    一行 ? 删除 + 一条账本行。原先的排除理由(PR #10546 持有该文件)已经过期(#10546 于 05:08:46Z 合并)——但换了一个新的持有者:

    ⚠️ 卡 #10547 目前在飞,持有的正是 packages/plugins/plugin-approvals/src/approval-service.ts。

    所以这一项的正确顺序是 #10547 落地之后再派。⛔ 不要因为「理由过期了」就当成没有栅栏 —— 栅栏还在,只是换了人。本席会在 #10547 合并后立即派出。

    ② 设计裁决 1 —— plugin-security 的 = {} 默认 sink

    已升维护者,⛔ 未裁前不可派。 选项与代价见 PR #10691 正文与 dev 的报告。

    ⚠️ 一条必须跟着这项走的更正:dev 上一轮报告的支撑测量两半都被他自己推翻了,完整更正立于 #10706:

    ⇒ 请按「潜在隐患、可达性未证实」看待。dev 主动下调了自己原先的建议分量:选项 C(字段诚实可选、不铸造会撒谎的 sink)不受影响;选项 D(把 logger 赋值提到早返回之上)现在只是整洁性 + 潜在隐患的论据,不是「实时安全报告消失」。

    ③ 设计裁决 2 —— service-settings 的 { error? }

    已升维护者,⛔ 未裁前不可派。 并且要求与 ② 合起来裁,理由是 dev 的测量而不是我的偏好:

    settings-service.ts 的三处报告点运行时并不静默 —— 每处都是 if (this.logger?.error) …; else console.error(message);而 config-change-audit.ts 已经在本地把通道加了回去(SettingsDiagnosticsLogger & { warn?: … },并在 :217 做 warn→error 降级)。⇒ no-fallback 是对类型的判词,模块用一个硬编码的 console 从旁边绕了过去。

    这就使 ② 和 ③ 是同一个问题:console 逃生口,还是一条声明出来的通道。分开答会让树里对同一个问题留下两个答案 —— 而这棵树已经因为「同一个名字下六份 MinimalLogger」(#10692)付过一次这个代价了。

    代价已数清:要求 warn 会让 6 个测试文件里 10 个单成员 { error } spy 失去可赋值性,每个补一个成员,机械。


    分诊接手时值得知道的两件事

    其一,Part of 是有门禁看着的。 #10691 上 Part-of PR must not also close its card 通过 —— 这不是我们私下的约定。

    其二,一条仪器事实(本轮踩到并记下):check-adr-0087-registration 从 git 读 changeset 内容,不读工作树。 dev 首次运行它得到 EXIT=0 且判词是「this PR adds no declared-breaking changeset」,而 **BREAKING** 就摆在文件里 —— 因为重写尚未提交。⇒ 「我改了 changeset,门禁是绿的」在提交之前不携带任何信息。


    Generated by Claude Code

  10. 19 remaining items

  11. 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

  12. 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

  13. 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

  14. 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

  15. 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

    Type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions