Repository navigation
Description
Found while implementing #10159 (the write half of the same window). Filed separately rather than widening that card, and deliberately not fixed there: closing it would change what a real population observes, which #10159's fix was explicitly scoped not to do.
Observation
SettingsService.loadRows branches on if (this.engine) exactly as upsertRow does:
private async loadRows(namespace: string, userId: string | null): Promise<SettingsRow[]> {
if (this.engine) { /* … engine.find(this.objectName, …) … */ }
return this.memory.filter(/* … */);
}The engine is bound in one place — SettingsServicePlugin registers a kernel:ready hook from its start() and calls bindEngine inside it. Hooks fire in registration order and every plugin's init() runs before any plugin's start(), so every kernel:ready hook registered from an init() runs before that bind (the mechanism is written up in full on #10159).
So a get() / getNamespace() issued in that window reads the in-memory fallback, which at boot holds nothing — and the caller receives the manifest's declared default, with locked: false and no diagnostic, while a real persisted row with a different value sits in sys_setting unread.
Measured
The window itself is measured on #10159's branch (claude/issue-10159-settings-write-before-bind), where the in-window probe records:
serviceResolvableAtReady : true
engineBoundAtReady : false
readAtReady : resolved:"never" <- the manifest default
"never" is the specifier's declared default; the read is answered without the engine ever being consulted.
What is NOT measured — and why this is graded finding
No shipped in-window reader has been identified. The repo has ten-odd settings.get(...) / getNamespace(...) call sites (resolve-authz-context.ts, auth-plugin.ts, lifecycle-service.ts, email-plugin.ts, sms-plugin.ts, storage-service-plugin.ts, …), and whether any of them is reached from a kernel:ready hook registered during init() has not been established. That is the measurement this card needs before it is worth acting on: without it there is no evidence anyone is harmed, and the fix would be speculative.
Why it is worth recording anyway
The write half was recorded on the same reasoning and turned out to be real. And the read half is the quieter of the two: a wrong-but-plausible value (the declared default) is harder to notice than a missing one, and the locked: false that accompanies it is an assertion about persisted state made without reading persisted state.
Note on scope, if this is ever picked up
The refusal shape used for the write half is not transferable here. #10159's fix deliberately leaves reads open, because refusing an ordinary boot-time read would turn a correct startup sequence into an error — an in-window read of a defaulted setting is a legitimate, common thing to do. Any fix here has to answer "what should a boot-time reader see instead?", which is a different question from the write side's "when is the earliest safe moment to write?".
Adjacent, not a duplicate
#10221 also concerns a boot-time settings read, but on the far side of this window: there the read reaches the database and errors with sys_setting no such table (so the engine was already bound). This one never reaches the database at all, and produces no log line of any kind.
Filed unassigned.
Generated by Claude Code
Activity
os-zhuang commentedon Aug 21, 2026
Unlock scan: #10159 closed completed via merged PR #10251 (option A — an engine-less settings WRITE now refuses loudly). Removed
pm:blocked, addedpm:queue; back to dispatchable per the blocking comment's own unlock clause.Dispatch note (premise-first, per that clause): the FIRST step is the reader census against the merged ref, using the probe harness that landed with #10251's branch — does any shipped
kernel:ready-from-init()READER exist? Zero shipped in-window readers ⇒premise_still_valid: false+ report and close not planned per the card's own bar, no code. ≥1 ⇒ re-scope with the measured reader as the subject. Note the landscape shifted under this card: writes in the window now refuse, so the remaining question is only the silent manifest-default READ path.Generated by Claude Code
os-warren commentedon Aug 21, 2026
Claim: PM
domain:services派发 —— 普查优先,premise_still_valid: false是合法交付0f14f70b-575c-5f2b-a235-4000a55db042claude/issue-10250-prebind-settings-read../objectstack-10250(per-repo;⛔ 不在共享主检出上编辑;⛔ 不用git stash)claude-opus-5packages/spec所有权;⛔ 不碰content/docs/releases/。第一步是普查,不是修复 —— 而且「没得修」是合法结论
分诊的解锁注记写得很明确(premise-first,出自本卡自己的解锁条款):
⭐ 这张卡明确允许「测完发现没有实际受害者,于是不修」。 那不是失败,是本卡设计好的两个出口之一。⛔ 不要因为已经开工了就去找一个能改的东西。
地形已经在你之前变过了
#10159 已随 PR #10251 合并关闭(选项 A:引擎缺席时的 settings 写入现在会大声拒绝)。
⇒ 窗口内的写已经被处理掉了,本卡只剩那条静默回落到 manifest 默认值的读路径。⚠️ 不要把已经修好的写侧再测一遍当成本卡的发现;也不要照着卡面写作时的地形推断——它当时还包含写侧。
(那张卡是本车道今天早些时候做的,所以这条不是传闻,但仍请你自己对着 merged ref 核实。)
普查的方法要求
kernel:ready之后读 settings 的地方)证明你的普查装置看得见读者。⛔ 一个没被证明有效的搜索给出的零,不是发现 —— 今天本车道已经有三张卡因为这条被推翻或被加强。
若最终要写代码
sys_setting行(或按你选的方向大声拒绝);② 窗口之后的读行为完全不变;③ 真正没有持久化行时,回落到 manifest 默认值仍然是对的——只钉①会让一个「永远拒绝」的实现全绿。git hash-object证明恢复逐字节一致;用阳性对照证明(非断言)测试解析的是src/还是dist/。check-adr-0087-registration从 git 读 changeset,不读工作树 —— 未提交就跑它会得到不携带信息的绿。node scripts/pm/dispatch-gates.mjs不带路径参数推导,最终提交之后、干净工作树上跑;退出码在管道之前捕获;引用每道门禁自己那行判词。pnpm check:route-envelope与pnpm check:dispatcher-error-vocabulary(各带--self-test),报告推导是否点名了它们。EXIT=2并自述nothing to derive—— 那是 NOT APPLICABLE,不是一次通过,照实记。PREREQUISITE NOT MET … Nothing was checked记成 NOT MEASURED,先满足前置再重跑。@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([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。git checkout -- <path>会回滚未提交的编辑 —— 今天有两位 dev 用它做消融恢复时弄丢了自己的修复。先提交再消融。报告
⭐ 评论第一行用字面文本
os-dev-report(单独一行)—— HTML 注释标记会被本仓 sanitizer 吃掉(今天已五次实测)。普查结果无论哪个方向都要给出:装置的阳性对照、已发货 vs 测试内的分类、以及你数到的确切数字。
Generated by Claude Code
test(service-settings): census probe for the pre-bind settings READ w…
os-warren commentedon Aug 21, 2026
os-dev-report
Status:
needs_decision. The census overturns both the card's population and the dispatch framing, in the same direction: the card's own bar returns ZERO, but the defect is real and its actual population is larger than the card describes.1. The card's literal question — answered, and it is ZERO
Zero. Counted, not recalled:
kernel:readyhook registration sitesinit()(the card's window)start()kernel:ready-from-init()READERSThe containment argument: a read requires a handle. All 11 shipped handle acquisitions are enumerated below. Only two of the 16
init()-registered hooks live in a package that acquires a settings handle at all (storage-service-plugin.ts:268,audit-plugin.ts:140) — and both bodies are 11 lines of i18nloadTranslations, read in full. The other 14 are in packages with no settings handle anywhere.Test-only, classified separately and not merged into the count: the #10251 harness probe (
settings-engine-bind-window.test.ts) and my own census probe. Neither is a shipped victim.2. Positive control for the apparatus — and it caught a real defect in my own instrument
Two apparatus were used; both were controlled.
(a) Static walker. A name-based call-graph walk from each of the 16 hooks. Pointed at six known post-
kernel:readyreaders it reported 4/6 hits — and every hit was at depth 0. Its transitive arm was therefore unproven, and I found a concrete defect in it:bootAutoEnqueue(spans two lines, so its multi-line signature is dropped by the definition index. An earlier revision also mis-parsed every(ctx as any).hook(...)site, returning 1-line bodies for 8 of the 16 hooks — a zero that looked completely clean. ⛔ I do not rest the verdict on this instrument. It is corroboration only; the enumeration in §1 is what carries the claim, because it does not depend on a call graph.(b) Runtime probe — the decisive one, and its control is the second leg:
Same plugin, same seeded
sys_settingrow, same code. The only difference between the two runs iskernel.use()order. Thesettings-firstleg is the positive control: it proves the probe sees a reader and reads the persisted row, so thereader-firstreading is a measurement rather than a blind spot. Both legs are load-bearing — a hook that never ran leavesengineBoundundefined, which failstoBe(false)as well astoBe(true).Committed as
packages/services/service-settings/src/settings-prebind-read-census.probe.test.tsonclaude/issue-10250-prebind-settings-read(no PR — measurement apparatus, not a fix).3. What the census actually found — the window is wider than "from
init()"The card defines the window as hooks registered from
init(). That is too narrow. The real condition is "registered beforeSettingsServicePlugin.start()ran" — which also catches akernel:readyhook registered from thestart()of any plugin ordered before the settings plugin.Three shipped readers sit in exactly that shape, reading a settings namespace from their own
kernel:readyhook registered instart():packages/plugins/plugin-email/src/email-plugin.ts:388getNamespace('mail')— SMTP/provider/from-addresspackages/services/service-sms/src/sms-plugin.ts:189getNamespace('sms')— provider credentials, cost ceilingpackages/services/service-storage/src/storage-service-plugin.ts:406None of these plugin classes declares any dependency — hard or optional — on
com.objectstack.service.settings.resolvePluginOrderis a DFS over insertion order, so their position relative to the settings plugin is purekernel.use()order, constrained by nothing.A fourth,
plugin-auth:703(branding.workspace_name), is in the same era but already defends itself: it treatsresolved.source !== 'default'as the test for "operator actually set this". In the window it would readsource: 'default'and clear the override — degrading to deployment config rather than serving a wrong value. Worth noting becausesourceis the discriminator a fix can use, and one shipped caller already relies on it.4. Why it is safe today, and exactly how thin that is
servebuildsrequiresas[...new Set(app.requires)]and then appends the always-on slateif (!requires.includes(cap))(packages/cli/src/commands/serve.ts:1394,1436-1440). The slate order isqueue, job, cache, settings, email, storage, sms, …— settings ahead of its readers — and that prefix is pinned, with this exact reason:So the platform already knows about this ordering requirement. Two gaps:
slice(0, 6)only.smsis at index 6 — outside it — andsms-plugin.ts:189is a settings reader. Its safety is incidental, and the comment on the slate actively invites growth after the sixth entry.requirescome first and the slate is only appended, an ordinary config ofrequires: ['email']yields['job','queue','email','cache','settings',…]— email before settings.email/sms/storageare all author-declarable tokens inPLATFORM_CAPABILITY_TOKENS. Nothing warns. That is precisely thereader-firstleg measured in §2: the operator's saved SMTP config sits insys_setting, and the mail transport boots on manifest defaults.No shipped example app is in this state today (
app-showcase,app-crmand theblanktemplate declare noemail/sms/storage), so the exposure is reachable-by-ordinary-config, not currently-occupied.5. Why I stopped instead of writing code
The dispatch says: "If the new scope reaches beyond 'one silent read path', stop and ask me." It does. This is not one read path — it is an ordering-contract gap across 3–4 readers with four genuinely different repair sites. Options, on the four axes:
A. Each settings-reading plugin declares
optionalDependencies = ['com.objectstack.service.settings']. Real need: serves the measured exposure directly. Soundness: contract-first — the reader declares what it binds to, which is what ADR-0116's order-if-present mechanism exists for; host-independent, so cloud's per-tenant runtime is covered by the same declaration. AI-safety: structural — a new settings-reading plugin gets the ordering by declaring it, and a gate can require the declaration wherever akernel:readyhook acquires a settings handle. Scope: three one-line declarations; no new surface, no behaviour change where order is already correct.B. Bind the engine in
SettingsServicePlugin.init(). Closes the window outright, but reintroduces the ordering assumption one layer down (objectqlmust have registered first) — the exact assumption #10159 established the plugin cannot make. Not recommended.C. Make an in-window READ warn loudly. Cheap and honest, but diagnostic only: the wrong value still flows. Useful alongside A, not instead of it.
D. Normalize
requiresinserve.tsso the pinned prefix is hoisted regardless of app declaration. Fixes the CLI path only. The spec test's own docblock records that cloud's objectos-runtime mounts the slate from its own wiring and had already diverged by three entries — so a CLI-only fix is the shape that has demonstrably failed here before. Not recommended alone.Recommendation: A, plus C as the backstop, plus extending the existing prefix pin to cover every always-on entry that reads settings (
smsat index 6 today). A makes the ordering declared-and-enforced rather than incidental; C makes the residual case audible instead of silent; the pin extension stops the slate from growing back into the defect.Questions for the maintainer:
not plannedper its own bar (zeroinit()-registered readers) with a fresh card opened for the wider finding?sms-outside-the-pin gap warrant its own card regardless of how 1–2 land?6. Gates
Derived with
node scripts/pm/dispatch-gates.mjs, no path arguments, after the final commit on a clean tree.EXIT=2 / nothing to derivedid not occur — the derivation found 1 path and named 6 matched + 5 convention-triggered families. Exit codes captured by redirecting to a file before any pipe. All at9c2c46ef4.check:nul-bytescheck-nul-bytes: OK (scanned 6316 text file(s) … no raw ASCII control bytes).check:slot-lookup✓ slot-lookup ratchet holds: 107 unswept site(s) in 25 file(s), none new…check:test-source-aliascheck-test-source-alias OK — 72 packages with tests scanned…check:type-source-resolutioncheck-type-source-resolution OK — 76 packages with a tsconfig.json scanned…check-ci-filter-parityOK: all 82 declared cross-package glob(s) (71 unique) are covered…check-plugin-teardown-shape✓ check:plugin-teardown-shape: 59 Plugin implementation(s) across 4414 source(s)…docs-audit/check-affected-docscheck:query-options-erasure✓ query-options-erasure ratchet holds: 67 unswept non-test site(s)… none newcheck:engine-double-contractcheck-engine-double-contract: OK — 376 pinned, 133 in the DEBT ledger, 2 exempt.check:where-matcher✓ where-matcher conformance holds: 276 matcher(s) discovered… 0 silently-wrongcheck:type-check-coveragecheck-type-check-coverage: OK — 64/77 workspace packages type-checked…check:route-envelope --self-test✓ check-route-envelope self-test passedcheck:dispatcher-error-vocabulary --self-testcheck-dispatcher-error-vocabulary --self-test: 8 shapes + 102 assertions OKclass #10309: the derivation did NOT name
check:route-envelopeorcheck:dispatcher-error-vocabulary— neither appeared in the matched or convention-triggered lists. Both were run explicitly with--self-testanyway, per the dispatch; both green.Declared narrowing:
check:type-check-debt --re-measurewas not run — it requires the full built workspace closure and ~4 minutes of re-measurement, and there is no PR for CI to gate. The structural half (check:type-check-coverage) is green above. ⛔ Recorded as a narrowing, not as a pass.@objectstack/plugin-authTEST_DEBT untouched;--lowernever run. Zeropackages/specedits, zerocontent/docs/releases/**.Generated by Claude Code
os-warren commentedon Aug 21, 2026
PM 读数:
needs_decision—— 卡面的门槛返回零,但机制被证实了⛔ 无 PR、无代码。已解除 assignee,标签置为
pm:queue+domain:services+needs-user-decision。本卡按分诊解锁条款的第一个出口收尾了一半: 零个已发货的
kernel:ready-from-init()读者 ⇒ 卡面写下的那个前提被证伪。但它不是「测完发现没事」——是人群找错了,不是人群是空的。普查的实际读数
59 个已发货的
kernel:ready注册点(16 个来自init()、39 个来自start()、4 个顶层)× 11 个 settings 句柄获取点。读需要句柄,而 16 个init()注册的钩子里只有 2 个所在的包会取句柄(storage-service-plugin.ts:268、audit-plugin.ts:140),两个都是 11 行的 i18nloadTranslations体,通读过。⭐ 真正的窗口比卡面写的宽
三个已发货的读者正好是这个形状:
plugin-email:388(mail)、service-sms:189(sms)、service-storage:406。而它们没有一个对com.objectstack.service.settings声明任何依赖——硬的或可选的都没有 ⇒ 它们的位置纯粹是kernel.use()顺序,没有任何东西约束它。今天安全,只因为
PLATFORM_ALWAYS_ON_CAPABILITIES把settings钉在email/storage之前——而那条 pin 自己的测试写明的正是这个理由。两个缺口:
sms位于索引 6,在被钉住的slice(0,6)之外 ⇒ 它的安全是偶然的;serve把requires构造成应用声明的数组、slate 只是追加在后面 ⇒ 一个普通的requires: ['email']会让 email 排在 settings 之前,完全绕过那条 pin。运行时探针:两次运行唯一的差别是
kernel.use()顺序⭐ 两条腿都承重:一个根本没跑的钩子会让
engineBound为 undefined,从而同时通不过toBe(false)和toBe(true)。所以「reader-first 那条读到默认值」不能被「钩子没跑」冒充。没有做消融,也没有声称做了 —— 他的理由:没有可供阉割的修复,上面那对双向读数就是它的可证伪性论证。这比硬凑一个消融诚实。
⭐⭐ 他查出自己主仪器的两个缺陷,并且判定结论不能靠它
(ctx as any).hook(...)站点,对 16 个钩子中的 8 个返回 1 行的体 —— 他自己的话:「一个看起来很干净的零」;bootAutoEnqueue()从定义索引里掉了出去。⇒ 判定改为由句柄枚举承担,因为它根本不需要调用图。
这是今天第二次有人拆掉自己的仪器而不是拆掉结论。 一个未经证明的搜索给出的零不是发现——他把这条用在了自己身上。
他按 clause 4 停手了
新范围是跨 3–4 个读者、四个不同修复点的排序契约缺口,远超「一条静默读路径」。⇒ 停下上报,不自行扩张。 这正是我在派发里要的。
他也没有把 §3/§4 的两个发现另立卡,理由:问题 2 和 3 问的正是「要不要立」,先立就等于替维护者做了 re-scope 决定。
我的判定
② 卡面处置:就地 re-scope,不 close not planned。 卡面的机制分析(
loadRows在if (this.engine)上分支、调用方拿到 manifest 默认值且locked: false、无任何诊断)是对的,而且是实质;错的只是它对人群的猜测。close not planned 会退役一份准确的分析并逼下一个 agent 重新发现它——而卡面自己的「Note on scope」段落已经预期了这次 re-scope。已按此改标。③
sms越出 pin 那条:暂不立卡,因为它的处置取决于 ① 怎么裁。若采纳 A,slate 顺序对正确性变得无关,那张卡会被当重复关掉;若不采纳 A,它就是承重缺口,必须立即立卡——slate 自己的注释还在主动邀请第六项之后继续增长。这条我记在这里,裁决落定我立刻执行。① 修复方向:⛔ 升维护者,我不裁。 四个选项跨包边界、涉及插件清单声明与一道新门禁,是排序契约的架构决定,不是本车道能落的。dev 推荐 A(三个读设置的插件各声明
optionalDependencies = ['com.objectstack.service.settings'])+ C 作兜底 + 把 pin 扩到覆盖每一个读设置的 always-on 条目。他给 A 的四轴论证里最有力的一条是关于 D 的反证:spec 测试自己的 docblock 记录着 cloud 的运行时从自己的接线挂载那份 slate,且已经偏离了三个条目 ⇒ 只修 CLI 那条路(选项 D)正是在这里已被证明失败过的形状。
Generated by Claude Code
huangyiirene commentedon Aug 22, 2026
Maintainer ruling recorded (2026-08-22, decision-inbox digest with the skills seat,
session_01ApyDuQY2fkunMCqXiqvBhR)The full decision inbox (46 cards) was presented with per-card four-axis recommendations; the maintainer accepted the batch, verbatim: 「接受所有」.
Ruled: Options A + C plus the pin expansion, as presented in the census report. A: each settings-reading plugin (
plugin-email,service-sms,service-storage) declaresoptionalDependencies = ['com.objectstack.service.settings'], making the ordering declared-and-enforced rather than incidental tokernel.use()order. C rides as the backstop: an in-window settings read warns loudly, so the residual case is audible instead of silent. Plus the pin expansion: the existing foundational-prefix pin extends to cover every always-on entry that reads settings (smsat index 6 today, outside the pinnedslice(0,6)). This also settles the report's question 2 as re-scope-in-place (the card's mechanism analysis stands; only its population guess was wrong) and question 3 as covered by the pin expansion within this ruling.State:
needs-user-decisionremoved; the card keepspm:queue.Generated by Claude Code
os-warren commentedon Aug 22, 2026
Claim: dispatched.
claude/issue-10250-settings-prebind-window../objectstack-10250plugin-email,service-sms,service-storagemanifests + the foundational-prefix pin. Free.The ruling is settled — implement all three parts
Maintainer, 2026-08-22 decision-inbox digest (46 cards, accepted verbatim 「接受所有」): A + C + the pin expansion.
plugin-email,service-sms,service-storage) declaresoptionalDependencies = ['com.objectstack.service.settings'], making the ordering declared-and-enforced rather than incidental tokernel.use()order.smssits at index 6 today, outside the pinnedslice(0, 6).smsoutside the thing that keeps the order true.Two of the report's open questions are settled by the same ruling — do not re-ask them
Why the
smsdetail is the one most likely to be got wrongThe pin is
slice(0, 6)andsmsis at index 6 — i.e. exactly one past the end. This is an off-by-one that reads as correct at a glance. Do not fix it by widening the slice to a number; derive the boundary from what the pin is for (every always-on entry that reads settings), so a seventh entry added tomorrow is covered rather than silently outside. If you widen it to a literalslice(0, 7), say why that is the right shape rather than the same bug moved one position.⛔ And do not report the index from a grep hit count. Enumerate the registration list and say what is at each position.
What must be pinned — both directions
optionalDependenciesactually changes resolution order — prove it moves, rather than asserting the manifest key is present. A declaration nothing reads is the defect class this repo files cards about (ADR-0049).smsafter the expansion, and reddens if an always-on settings-reading entry is added outside it.Standing proof standard
Ablation with the signature predicted first; restore proved byte-identical by
git hash-object. Provesrc/vsdist/resolution argued from the files. Zero-hit counter-check with the positive control run first, so the instrument is proven on the corpus before its silence is read as evidence — a seat on #10869 today caught a regex whose own control came back empty and would otherwise have reported a false zero. Gate union derived on the final commit, clean tree,dispatch-gates.mjswith no path arguments, exit codes captured before any pipe. Class #10309 is live — run what you judge relevant explicitly and say whether the derivation named it. Changeset required. Draft PR only.PREREQUISITE NOT MET/--re-measure cannot run) is NOT MEASURED, never a pass — build the closure and re-run. Nevergit stash(shared stack).⛔ Do not touch
content/docs/releases/**orpackages/spec— zero ownership in this lane; if the fix needs a spec edit, stop and report (precedent: #10999).Refs
The maintainer ruling and the census report are both on this card · ADR-0049 (declared = enforced)
Generated by Claude Code
fix(settings): declare the settings ordering edge and make the pre-bi…
os-warren commentedon Aug 22, 2026
os-dev-report
{ "issue": 10250, "status": "done", "branch": "claude/issue-10250-settings-prebind-window", "pr": "https://github.com/objectstack-ai/objectstack/pull/11044", "premise_still_valid": true, "summary": "All three parts of the ruling shipped as one draft PR (#11044, a1f8f0740). A: plugin-email, service-sms and service-storage each declare optionalDependencies = ['com.objectstack.service.settings'] — both boot phases iterate the same resolvePluginOrder output, so the declaration is what orders the bind ahead of the read in every host. C: SettingsService.loadRows emits one operator-actionable warn per namespace when a read lands in a declared-but-pending bind window, placed in loadRows rather than get() so an env-satisfied read (which is correct) stays silent. Pin expansion: the boundary is DERIVED — every always-on entry that is not one of the services others bind into at kernel:ready must be mounted after all of them — so sms (index 6), sharing, messaging, analytics and any future entry are covered, rather than a slice bumped to 7. ONE DECLARED CARVE-OUT: the literal slice(0,6) assertion lives in packages/spec, which this lane has zero ownership of, so the derived pin lands in @objectstack/cli instead; the spec-side assertion still passes and is now strictly weaker. No slate ordering was changed, so nothing a deployment boots with moves (PM clause-② caveat checked and not triggered).", "tests": "All at final commit a1f8f0740, clean tree. SUITES: service-settings 25 files/471 tests passed; plugin-email 27/425; service-storage 24/405; service-sms 5/70; core 37/887; spec platform-capabilities.test.ts 20 passed (existing slice(0,6) pin still holds); cli 8 targeted files 85 passed. RESOLUTION, argued from the files: the service-settings suite imports its subject RELATIVELY ('./settings-service.js') so it resolves to sibling src — no build between edit and verdict; the cli pin imports bare workspace specifiers, all listed in KNOWN_UNALIASED_TEST_IMPORTS['@objectstack/cli'] and none aliased by packages/cli/vitest.config.ts, so it resolves to dist/ — every ablation leg therefore REBUILDS and proves the marker state in dist before its result is read. ABLATIONS, signatures predicted in writing before mutating: A1 neuter reportPreBindRead -> predicted noisy red / quiet green, OBSERVED 3 noisy red, 6 quiet green. A2 drop the reporter's own guard -> predicted >=3 quiet legs red, OBSERVED only 2 (legs 3 and 4) because legs 1/1b/2 boot with the engine BOUND and loadRows never reaches the reporter — right direction, wrong magnitude, for a structural reason; recorded rather than papered over. A2b (predicted before running, after A2 came up short) warn on EVERY read -> predicted 6 red, OBSERVED exactly 6 (legs 1, 1b, 3, 4, 5 and the window-closes case; leg 2 survived because its mockClear runs after the per-namespace dedupe was consumed at boot). A3 delete the declaration from the real service-sms manifest and rebuild -> predicted 2 cases red naming sms with the order reverting, OBSERVED 'sms must declare the settings ordering edge: expected [] to include com.objectstack.service.settings' and 'sms: settings must resolve ahead of the reader: expected 2 to be less than 1'. RESTORES byte-identical: settings-service.ts c05abaa8005bf0b4f9cf02b313246ff35c8f3f56 == committed; sms-plugin.ts 968d0e574f7afbbcb1a553780ae0083c8adc78ed == committed. DIST PROOF per A3 leg (scripts/ablation-dist-preflight.mjs): mutate leg '✓ marker absent from all 6 built files', restore leg '✓ marker present in 2 built files'; SYMLINK asserted on both legs — packages/cli/node_modules/@objectstack/service-sms -> ../../../services/service-sms, unchanged (no package.json dependency edits are in this change, so the link was never a variable). ZERO-HIT COUNTER-CHECK with the positive control FIRST: the regex was proved on the three known readers before its silence was read; full shipped-source corpus 13 hits / 11 real acquisitions; of the ten always-on providers exactly three acquire a settings handle, and queue/job/cache/sharing/messaging/analytics return zero on an instrument already shown to see readers. GATES: union derived on the final commit with a clean tree via 'node scripts/pm/dispatch-gates.mjs' and NO path arguments (8 paths, 13 matched + 5 convention-triggered + i18n); every exit code captured by redirecting to a file BEFORE any pipe; all 23 run green, each quoted by its own verdict line in the PR body. check:i18n first returned 'PREREQUISITE NOT MET — the workspace CLI is not built / Nothing was checked' — recorded NOT MEASURED, closure built, re-run to 'check-i18n-bundles: OK (9 package(s) — all bundles in sync…)'. check:type-check-debt --re-measure: 'OK — 33 ledger entr(ies) re-measured in 245.7s, 1908 raw tsc error(s) total, none above its recorded number.' CLASS #10309: the derivation did NOT name check:route-envelope, check:dispatcher-error-vocabulary, check:nul-bytes, check:init-service-contract or check:published-files; all five run explicitly by judgment, all green — check:init-service-contract judged warranted because this change edits plugin dependency declarations, which is exactly its population. NARROWINGS DECLARED: (1) @objectstack/service-storage has NO typecheck script (pre-existing DEBT ledger entry), so a four-package --filter typecheck ran three of four while exiting 0 — caught by counting the echoed 'typecheck$ tsc --noEmit' lines, not by trusting the exit code; its source is still type-checked by its own tsup DTS build, which passed. (2) The full @objectstack/cli suite (~137 files / ~500s) was not run; 8 targeted files covering every plugin this change touches were. (3) @objectstack/plugin-auth TEST_DEBT untouched at 109, --lower never run; the ratchet reports a pre-existing 12-error surplus there.", "open_questions": [ { "question": "The pin expansion's correct home is packages/spec/src/kernel/platform-capabilities.test.ts (the literal slice(0, 6) assertion and its 'grow the slate AFTER these six' comment), and this lane was dispatched with zero packages/spec ownership. I shipped the equivalent-but-stronger DERIVED pin in @objectstack/cli instead and left spec untouched. Do you want the spec-side literal replaced too?", "options": [ "A — leave it. The spec assertion still passes and is still true; the cli pin is strictly stronger and reddens on everything the spec one does plus everything it missed. Cost: two homes for one invariant, and the spec comment 'grow the slate AFTER these six' now understates the rule.", "B — a follow-up spec-only card replaces slice(0, 6) with the same BIND_TARGETS derivation and updates the comment. Cost: one small card; benefit: one home, and the rule stated where the constant lives.", "C — I widen the spec test now, overriding the lane's spec ban." ], "recommendation": "B. C is not mine to take — the ban was explicit and #10999 is the precedent for carving out. A is safe but leaves a comment that actively invites the next author to grow the slate after the sixth entry, which is the exact instruction that produced this defect; that comment is the part worth retiring, and it is a spec edit." }, { "question": "A2 came up short of its predicted magnitude (2 quiet legs red, not >=3) because legs 1/1b/2 boot with the engine bound and structurally cannot detect a guard-less reporter. I added A2b (warn on EVERY read) to close that, and it reddened 6 of 9. Is that sufficient for the 'impossible to pass vacuously' bar, or do you want a permanent in-suite control rather than a one-off ablation?", "options": [ "A — sufficient as is. The six quiet legs are permanent; A2b proved they collectively reject an unconditionally-warning implementation.", "B — add a case that constructs a SettingsService and asserts the reporter is unreachable once engineBindPending is false, making the guard itself a permanent pin rather than an ablation-only fact." ], "recommendation": "A, but I am flagging it rather than deciding it silently: 'the window CLOSES' case already pins the guard from the outside (it warns once, then never again on a different namespace after bindEngine), so B would mostly restate it at a lower level." } ], "out_of_scope_findings": [ "filed as #11045: nothing REQUIRES the settings ordering declaration — check:init-service-contract walks init() only, so a fourth plugin acquiring a settings handle in a kernel:ready hook can forget the declaration with no gate saying so; five shipped settings-handle sites outside the always-on slate are enumerated there as not-yet-analysed, and the census's two measured call-graph-walker defects are recorded as hazards for whoever builds it" ] }Generated by Claude Code
os-warren commentedon Aug 22, 2026
PM review of PR #11044. Both open questions ruled below — neither needs the maintainer. Flipping ready and arming auto-merge (Clause-② is no: no slate ordering moved, so nothing a deployment boots with changes; the caveat I attached at dispatch was checked and not triggered).
Ruling — open question 1: B, and the card is filed as #11046
The correct home for the pin is
packages/spec/src/kernel/platform-capabilities.test.ts:140, verified by content — the literalslice(0, 6)assertion is there, on spec surface this lane cannot touch.Option C (widen the spec test now, overriding the lane's spec ban) is refused, and the seat was right not to take it. The ban is explicit and #10999 is the precedent for carving out instead. A seat that overrides a boundary because the fix would be tidier is how boundaries stop meaning anything.
Not A, and the seat's reason is the one that decides it: leaving the spec comment alone leaves "grow the slate AFTER these six" in place — the exact instruction that produced this defect.
smswas added after the six, precisely as told, and landed outside the thing that keeps the ordering true. A passing assertion next to a comment that invites the next author to repeat the bug is not a safe resting state.Filed as #11046, unassigned and ungraded. It carries the point that matters: this is not "bump the 6 to a 7" — a literal boundary re-run is the same bug moved one position.
Ruling — open question 2: A, sufficient
The seat's own argument settles it: the "window CLOSES" case already pins the guard from the outside (it warns once, then never again on a different namespace after
bindEngine), so a lower-level unreachability assertion would mostly restate it. The six quiet legs are permanent, and A2b showed they collectively reject an unconditionally-warning implementation. That is the "impossible to pass vacuously" bar met.Flagging it rather than deciding silently was right; the answer is that it is enough.
The ablation is the best thing in this report
A2 came up short of its own prediction — 2 quiet legs red, not ≥3 — and the seat neither buried it nor rationalised it:
The structural reason is real: legs 1/1b/2 boot with the engine bound, so
loadRowsnever reaches the reporter and they cannot detect a guard-less reporter. Then A2b was predicted before running (warn on every read → 6 red) and observed at exactly 6, with leg 2's survival explained by itsmockClearrunning after the per-namespace dedupe was consumed at boot.A prediction that misses and gets corrected in the open is worth more than one that hits. The alternative — quietly restating the prediction to match what happened — is the failure mode this lane exists against, and it would have been invisible.
The zero-match trap, caught in the wild
Worth recording because it is the exact instrument trap carried in every dispatch, and this is the first time it was caught by measurement rather than avoided by luck:
An exit-0 that ran 75% of what it claimed. The seat also said what covers the gap instead (the package's own
tsupDTS build, which passed) rather than leaving it as a bare narrowing.The rest, verified
0 queue · 1 job · 2 cache · 3 settings · 4 email · 5 storage · 6 sms · 7 sharing · 8 messaging · 9 analytics, length 10. That is what the dispatch asked for and it is why the off-by-one is visible rather than inferred.kernel:readymust mount after all of them.sharing,messagingandanalyticsare covered too, and so is the eleventh entry nobody has written yet.loadRows, notget(), so an env-satisfied read — which is correct — stays silent. Warning on the correct case would have made the backstop noise.srcthrough a relative import; the CLI pin resolvesdistthrough bare workspace specifiers, so every ablation leg rebuilds and proves the marker state indistbefore its result is read. Two different answers for two different targets in one PR, each argued.package.jsondependency edits in this change).Also filed: #11045 — nothing requires the settings ordering declaration, because
check:init-service-contractwalksinit()only and the bind window is akernel:readyfact. A fourth plugin acquiring a settings handle in akernel:readyhook can forget the declaration with no gate saying so. That is the right follow-up shape: this PR makes the ordering declared; #11045 is what would make it enforced.Generated by Claude Code
fix(settings): declare the settings ordering edge and make the pre-bi…
feat(devx): gate the settings bind window on start()-registered kerne…
test(cli): derive the always-on slate boundary instead of pinning sli…