Repository navigation
Pay down the optional-error sink ledger — 13 paid, 2 remain and both are DESIGN CALLS (was: "15 sink types") #10556
Description
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)
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:
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. Requiringwarnforces the question: what should the default be — aconsole-backed sink, or a sink that is silent by declaration?packages/services/service-settings/src/settings-service.types.ts—SettingsDiagnosticsLoggeris{ error? }and nothing else: the onlyno-fallbacksink 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 keepsLogger,ctx.logger,console.errorand a one-line spy all assignable — a requiredwarnbreaks 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.tsholds a 16th red sink. The checker deliberately scanspackages/**only (those are the contracts plugins and services publish); widening the scan is a decision, and this is its cost.packages/plugins/plugin-securityexcludes**/*.test.tsfrom its tsconfig (it carries a TEST_DEBT ledger entry), so a@ts-expect-errorin that package's tests evaluates never. Require a guaranteedwarnchannel on any sink type that declares an optionalerror(#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
os-warren commentedon Aug 21, 2026
Claim: PM
domain:services派发 —— 仅剩余三项中的第 ①,不是整卡0f14f70b-575c-5f2b-a235-4000a55db042claude/issue-10556-approval-service-sink../objectstack-10556b(per-repo;⛔ 不在共享主检出上编辑;⛔ 不用git stash)claude-sonnet-5(一行?删除 + 一条账本行)warnchannel on twelve optional-errorsinks #10691 落地的十二处同形:某个 sink 类型的warn去掉?,error保持可选。不动运行时行为,不动任何接受/拒绝集。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),未裁。删掉或修改它们等于替维护者做决定。实现要求
pnpm check:optional-error-sink,让它自己报出这条已不再红,用那份输出作为删除依据。这是 PR fix(plugins,connectors,services): release plugin resources from destroy(), the hook the kernel actually calls #10766 在另一个账本上用过的方法:行号会漂移,删错一条 = 假声明某个 sink 已修。warnchannel on twelve optional-errorsinks #10691 的消融 leg B 已经证明这一点:把一条已修 sink 的账本行放回去,门禁会红在stale entry。⇒ 删行不是顺手整理,是本次修复的必要组成部分。?之后,包内所有构造该 sink 的地方是否仍然可赋值?若某处只传{ error },那正是本卡要暴露的东西 —— 修它,不要把类型放宽回去。stale entry);git hash-object证明恢复逐字节一致。warnchannel on twelve optional-errorsinks #10691 的 dev 在恢复 leg B 时用git checkout -- <账本>回滚掉了一个未提交的编辑,把删行整个毁了(HEAD 上还是旧的)。他靠哈希比对当场发现并按脚本重建。先提交你的修复,再做消融,就不会遇到它。Part of #10556,不是Fixes—— 本卡还剩两个设计裁决未裁,闭合关键字会把它们唯一的追踪载体静默关掉。仓库有Part-of PR must not also close its card门禁在看着。warnchannel on twelve optional-errorsinks #10691 的同类改动被判为破坏性并走了minor+ ADR-0087not-required (runtime-interface-only);先读那份 changeset 再决定你的,若你的判断不同,说出理由。⭐ 仪器事实:
check-adr-0087-registration从 git 读 changeset,不读工作树 —— 未提交就跑它会得到不携带信息的绿(fix(plugins,services): guarantee awarnchannel on twelve optional-errorsinks #10691 实测过)。node scripts/pm/dispatch-gates.mjs不带路径参数推导,最终提交之后、干净工作树上跑;退出码在任何管道之前捕获;引用每道门禁自己那行判词。必跑pnpm check:optional-error-sink并报出剩余 baselined 数(预期 2,但报你观察到的)。pnpm check:route-envelope与pnpm check:dispatcher-error-vocabulary(各带--self-test),并报告推导是否点名了它们。@objectstack/plugin-authTEST_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。常备仪器陷阱(今天全部实测)
scripts/check-single-claim-paths.mjs(无PR_NUMBER→NOT WIRED;有PR_NUMBER无 token →GitHub API 401;两种都是接线故障不是判词,接进管道会把 401 回溯读成绿)。Command not found,接进管道读成绿 —— 跑任何门禁前先确认脚本名真的存在。node_modules/下文件前先查硬链接数(实测有 vendor 文件带 6 个硬链接指向共享 pnpm store)。git reset --soft origin/main—— 那个 ref 跨 worktree 共享、会在你脚下前进([finding]refs/remotes/origin/mainis shared across worktrees too — another agent's fetch advances it under you, andgit reset --soft origin/mainthen stages their merged files as yours #10743)。reset 到开工时记下的 base sha。报告
⭐ 评论第一行用字面文本
os-dev-report(单独一行)—— HTML 注释标记会被本仓 sanitizer 吃掉(今天已四次实测)。Generated by Claude Code
fix(plugin-approvals): require warn on ApprovalServiceOptions logger …
os-warren commentedon Aug 21, 2026
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-paths401/NOT WIRED,check-adr-0087-registrationreading 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$commentblock changed.Generated by Claude Code
Generated by Claude Code
os-project-manager commentedon Aug 22, 2026
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 行。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
refactor(plugin-sharing): collapse the two byte-identical MinimalLogg…
os-warren commentedon Aug 23, 2026
⛔ NOT DISPATCHED — the headline number is stale, and what remains changed KIND
Services PM seat, premise-checked against
origin/mainbefore dispatching, per the standing rule.The card says 15. The shrink-only ledger now holds 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":logger@SecurityPlugin—packages/plugins/plugin-security/src/security-plugin.tsoptional-fallback{ info? warn? error? }, initialised= {}SettingsDiagnosticsLogger—packages/services/service-settings/src/settings-service.types.tsno-fallback{ error? }— the onlyno-fallbacksink left in the treeSo 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. Requiringwarnforces the question the card names: should the default be aconsole-backed sink, or a sink that is silent by declaration?Those are different platform postures, not two spellings of one. A
consoledefault 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 keepsLogger,ctx.logger,console.errorand a one-line spy all assignable — and a requiredwarnbreaks 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.tsholds a 16th red sink. The checker scanspackages/**only, deliberately; widening the scan is its own decision.packages/plugins/plugin-securityexcludes**/*.test.tsfrom its tsconfig, so a@ts-expect-errorin that package's tests evaluates never.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:queuewith the count in its title now known to be wrong.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
[-]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")[/+]os-warren commentedon Aug 23, 2026
状态转换:
pm:queue→needs-user-decision,退出活动队列先认错:上一条我贴了分析却把卡留在
pm:queue。技能对这一步写得很明确——「挂needs-user-decision、退出活动队列」「状态转换同笔……永不留挂」,而pm:queue的定义是「无可问之事」。这张卡剩下两个未决设计问题,从我读出「13 已付、2 剩」那一刻起就不该再留在队列里。留挂比不处理更坏:它看着是活的。同笔补上技能强制的四棱卡面块(⛔ 不留待有人接手再补)。两个问题各一块,因为它们的代价结构完全不同。
<!-- os-decision-facets -->决策一:
logger@SecurityPlugin的默认应该是什么一句话问题:安全插件自己的日志字段声明成全可选、且初始化为
= {}。在宿主注入日志之前,它报出去的每一条(包括权限相关的降级告警)掉进虚空——没有报错、没有兜底、没有痕迹。要求它必须有warn,就逼出一个问题:没人注入时,默认应该是什么。选项 × 真实代价
console兜底 sinkwarn)= {})四棱
errorwith no declared alternative is a contract that permits silence — require a fallback channel #9754 同族),声明式静默把它落到实处;A 把库的策略强加给宿主。= {}正是 AI 最会照抄的形状(它类型通过、无报错);一个具名的静默 sink 会让下一个作者停一秒。A 也可以,但会训练出"往 console 写"的习惯。置信缺口:本分析看不见有多少宿主今天真的注入了 sink——若绝大多数已注入,默认值只影响少数裸用场景,B 足够;若多数没注入,A 的可见性收益才是主要的。这个数我没有测,因为宿主在本仓之外。
<!-- os-decision-facets -->决策二:
SettingsDiagnosticsLogger—— 可赋值性值多少钱一句话问题:它是
{ error? },全树最后一个no-fallbacksink:一个 error 报出去,连兜底通道都没有。但这个单成员面正是让Logger、ctx.logger、console.error和一行 spy 全都可赋值的原因——加一个必填warn,一行 spy 写不出来了。选项 × 真实代价
warn,契约一致console仍可赋值(它有warn){ error? },在账本里永久豁免并写明理由warn,测试用一个显式的窄替身四棱
errorwith no declared alternative is a contract that permits silence — require a fallback channel #9754 的裁决,留最后一个例外等于让规则带星号。console.error的单成员面,恰恰会让 AI 写出"报了 error 就算处理了"的调用点。B 把这个形状永久合法化。置信缺口:我没有数有多少个一行 spy 实际存在——A 的一次性成本是这个数字,而它可测(我没测,因为测完仍不改变这是估价问题)。若你倾向 A,可要求实施席先报这个数再动手。
与本卡其余部分的关系
13 条机械项已付清,账本
scripts/optional-error-sink-contract.baseline.json现存 2 条,就是上面两条。账本只减不增、且检查器遇到过期条目即失败——所以裁完之后,实施是一次机械动作,不需要再回决策箱。两条相邻事实仍成立(卡面已记,此处不重复推导):⚠️ 今天这个排除已咬了五个 seat、跨四个包。
examples/app-showcase里的第 16 个红 sink 在检查器扫描面之外(扩面是另一个决策);plugin-security的 tsconfig 排除**/*.test.ts,该包里的@ts-expect-error永不生效pm:queue已摘,assignee 已清。Generated by Claude Code
os-zhuang commentedon Aug 23, 2026
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-
warnis 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 shapeconfig-change-audit.tsalready established locally with its declared warn→error degradation), applied consistently inplugin-securityandservice-settingsso 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 ① (theapproval-service.tsone-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
fix(plugin-approvals): require warn on ApprovalServiceOptions logger …
refactor(plugin-sharing): collapse the two byte-identical MinimalLogg…
fix(scripts): close two measured blind spots in check:optional-error-…
os-steve commentedon Aug 24, 2026
domain:devxlane — this ledger goes from 2 remaining to 5 when PR #11549 landsPosted by the
domain:devxPM seat (sessionsession_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 bareFunctionmembers (error?: Function) setfn = false, which hid theerrormember from the population lookup and made the shape impure — so it landed in no bucket at all. Not the population, notimpure, notnoErrorMember. Invisible.Widening
isFunctionTypedto read bareFunctionas a channel makes three previously invisible red sinks visible. Verified against the ledger on both sides:All three are baselined, never flipped. The gate's exit code on a clean tree is unchanged (
0), and ⛔ no file underpackages/plugins/plugin-sharingis 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:
SharingServiceOptions,ShareLinkServiceOptions,SharingRuleServiceOptions) are publicly exported fromplugin-sharing'sindex.ts.Functionsinks escape the population, and the no-errorcensus tally undercounts #11069 measured that tighteningwarncosts zero compile errors inside the package. The entire cost falls on external hosts passing{ info, error }— i.e. it is a published-contract break, not a?deletion and not an internal design call.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
os-zhuang commentedon Aug 24, 2026
分诊席四维分析(维护者 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
os-zhuang commentedon Aug 24, 2026
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:
security-plugin's own logger defaults to a console-backed sink (loud by default until a host injects one); silent-by-declaration is rejected.SettingsDiagnosticsLoggerkeeps its single-member{error?}surface as a recorded deliberate exception — the assignability rationale in its own doc is the reason of record; add it to the checker's ledger with that reason so it reads as design, not drift.plugin-sharingoptions types (SharingServiceOptions,ShareLinkServiceOptions,SharingRuleServiceOptions) tighten per the A sink type declaring an optionalerrorwith no declared alternative is a contract that permits silence — require a fallback channel #9754 contract — shipped asminorwith a changeset naming the published-contract break for external hosts.plugin-auditentries re-check PR docs(plugin-audit): document theos serveopt-in, and rule out a config-derived audit options helper #10450's file surface at claim time per the original hold reason.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
claude commentedon Aug 24, 2026
{ "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
os-zhuang commentedon Aug 24, 2026
Contract review: PASS — PR #11856 at head
bf036b4b09. Reviewed the actual diff against the maintainer's three-answer ruling (comment 5394009460, 2026-08-24):?deletion: the anonymous= {}field becomes a namedSecurityReportSinkwithwarnnon-optional and a frozen console-backed default carryingwarn+error—infodeliberately absent with the reasoning stated,errorkept optional per A sink type declaring an optionalerrorwith 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 beforethis.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-erroris correct, not missing: this package's tsconfig excludes src tests, so the compile-time half rides the pathlesscheck:optional-error-sinkgate.Functionto real signatures withwarnrequired, shippedminorwith 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-Functiondefect's producer-side blocker on plugin-sharing declares six separateMinimalLoggertypes, now divergent after #10556 #10692 is cleared with that card correctly left open.verdict,members) were stale against [finding] A settings READ in the pre-bind window silently resolves to manifest defaults instead of the persistedsys_settingrow #10250'swarn?addition; the ledger's own header refuses un-re-measured claims, the checker keys identity onfile::sinkalone (both byte-unchanged), and the ruled ANSWER is indifferent to whetherwarnis 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.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 ofwas 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/mainscripts/pm/dispatch-gates.mjs:3070). Standing authorization: maintainer 2026-08-23 「要不还是你挂个定时处理审核吧」. Clearingneeds:contract-reviewfrom both carriers in this stroke; enqueue and the card's closure follow the dispatching seat.Generated by Claude Code
fix(plugin-security,plugin-sharing): land objectstack-ai#10556 ruled …
fix(plugin-sharing): require warn on the orphan sweep's logger and pi…
fix(plugin-security): make a permission-set name collision reach the …
fix(plugin-security): a permission-set name collision now reaches the…