Repository navigation
settings 消费缝丢弃 ResolvedSettingValue.source —— 服务无法区分「管理员写过的值」和「schema 默认值」 #5536
Description
Activity
发现分诊轮判级(services 车道 PM,2026-08-05):持有(留
finding),锚定维护者 #4968 裁决的 Q2 条款 —— storage 受害路径由 #4968-B(生产者侧统一 env 通道)修复,「消费缝保留source」的平台级语义待第二个实证受害路径出现再立 ADR/实施。sms/email 目前无已证实的用户可见故障。下轮分诊轮复核;#4968-B 落地后顺带复验 storage 侧是否仍需读 source。
Generated by Claude Code
发现分诊轮(#4949 纪律):维持持有(留
finding),域维持domain:services。上一轮留的「#4968-B 落地后顺带复验」本轮已执行,结果记录如下。复验结果(
origin/main@9e3709a)- OS_STORAGE_ROOT 对任何非默认值不生效:CLI 与设置服务的 env 通道错位,schema 默认值覆盖宿主构造配置(原报「swap 假警告」归因已被 #4096 时间线推翻,警告本身是准确的) #4968 已 closed/completed ⇒ 复验触发;
- 三个消费缝一处未变,
source仍在同样的位置被丢掉:service-storage/src/storage-service-plugin.ts:397(values[k] = v?.value;)、service-sms/src/sms-plugin.ts:157(同形)、settings-service.ts的snapshotOf—— 正文的:472现为:696(raw[k] = v.value,行号漂移,形状一致); - storage 的判据也未变:
hasAny仍是「值非空」测试(:400-401),紧邻注释仍写 “No persisted values yet → keep the constructor-built adapter” ⇒ 注释说 persisted、代码测 present 这一处注释与代码的错位仍在。
⇒ 读数很明确:#4968 是在生产者侧(统一 env 通道)把 storage 那条受害路径修掉的,没有让消费缝开始读
source。所以本单的平台级语义(消费缝保留source/ 派生authored)一条也没被顺带解决,原持有理由完整存续。判级:持有 —— 重启条件(维持维护者 #4968 Q2 口径,并补一条可机械判定的)
- 出现第二个实证受害路径(sms / email / 任一 settings-bound 服务上,「schema 默认值被当成有人配过的值」造成用户可见故障)⇒ 平台级语义有了第二个证据点,按 OS_STORAGE_ROOT 对任何非默认值不生效:CLI 与设置服务的 env 通道错位,schema 默认值覆盖宿主构造配置(原报「swap 假警告」归因已被 #4096 时间线推翻,警告本身是准确的) #4968 的两选项立 ADR 并晋级;
- 或下一次有人动这三个缝中任意一个所在文件时,把
source的保留作为同函数收尾一并做掉(那时它不需要单独立项,只需在那单里点名本条); - ⛔ 不要只修 storage 的
hasAny判据 —— 单点把「非空」换成「非默认」会让三个缝的口径进一步分叉,而分叉正是本单要治的东西。
本评论来自分诊座位 Routine(#5474 试点),不构成认领。
Generated by Claude Code
Findings triage round (#4949 discipline): HOLD maintained (
findingkept), domain staysdomain:services. Stale-premise check @origin/main80f7dc6. Restart condition ② fired since the last grading and was not taken — recorded here so it is a fact on the issue rather than a missed window nobody can see.Restart condition ② — fired, missed
The 08-06 18:01Z grading wrote: "or the next time anyone touches one of these three seam files, keep
sourceas a same-function wrap-up in that PR."packages/services/service-sms/src/sms-plugin.tswas touched:9c90ea0b(feat(sms): 短信全局/每租户日发送配额(成本总量闸) #2814 / PR feat(sms): 短信全局日发送配额 —— 成本总量闸 (#2814) #6042, 2026-08-06T19:59:31Z, SMS global daily send quota) — after the 18:01Z grading;- the seam is unchanged by it: still
values[k] = v?.value;, line drifted 157 → 197; - the other two seams did not move at all (REST commit list for
service-storage/src/storage-service-plugin.tsandservice-settings/src/settings-service.tssince 2026-08-06T13:00Z is empty), and both read as they did last round:storage-service-plugin.ts:397(values[k] = v?.value;) withhasAnystill the value-is-non-empty test at:400-401, andsettings-service.ts:696(raw[k] = v.valueinsidesnapshotOf).
Condition ② is opportunistic and does not expire — a missed window costs only the marginal cost of the next one, so this does not change the grade. But it is worth one line on the record: the condition has now demonstrably fired once without being noticed, which is the failure mode of any "someone will do it while they are in there" condition. The mechanism available to this seat is exactly this comment; the executing lane that next claims work in
service-sms/service-storage/service-settingsshould be pointed at this issue at claim time.Verdict: HOLD — restart conditions unchanged
- A second demonstrated victim path (sms / email / any settings-bound service, where "the schema default was taken for a value someone configured" causes a user-visible fault) ⇒ the platform-level semantics have their second evidence point; open the ADR per OS_STORAGE_ROOT 对任何非默认值不生效:CLI 与设置服务的 env 通道错位,schema 默认值覆盖宿主构造配置(原报「swap 假警告」归因已被 #4096 时间线推翻,警告本身是准确的) #4968's two options and promote.
- Or the next PR that touches any of the three seam files keeps
source(or derivesauthored: boolean) as a same-function wrap-up — fired once already (feat(sms): 短信全局日发送配额 —— 成本总量闸 (#2814) #6042), still available. - ⛔ Still do not fix only storage's
hasAnypredicate — swapping "non-empty" for "non-default" at one seam widens the divergence between the three, and the divergence is the thing this issue exists to treat.
本评论来自分诊座位 Routine(#5474 试点),不构成认领。
Generated by Claude Code
os-project-manager commented
on Aug 8, 2026 CollaboratorMore actionsFindings sweep (maintainer-authorized one-off, 2026-08-07 — registered on #6015): hold confirmed.
ResolvedSettingValue.sourceis declared-with-purpose but unread outside the one victim path #4968 already fixed. Restart: the next consumer defect that needs the admin-value vs schema-default distinction — wire it then, or retire the field if a full year passes without one.
Generated by Claude Code
Findings triage round: HOLD maintained (
finding+domain:serviceskept). Stale-premise check @origin/mainb88f5e8: all three consumer seams unchanged in shape —service-storage/src/storage-service-plugin.ts:397andservice-sms/src/sms-plugin.ts:197still readvalues[k] = v?.value;, andsettings-service.ts'ssnapshotOfloop drifted:696→:1195(raw[k] = v.value, same shape).Restart condition ② fired a second time and was again not taken:
settings-service.tswas touched bye2798fa(#6345 / PR #6910, driver-vocabulary convergence — after the 08-08 grading) without keepingsource. That is twice now (first: #6042 on sms, recorded 08-07). Two missed opportunistic windows do not change the grade — the condition is designed not to expire — but the pattern the 08-07 round named ("someone will do it while they're in there" conditions fire silently) now has two data points. Executing lanes claiming work inservice-settings/service-storage/service-sms: take the ride-along and name this card.Restart conditions unchanged: ① a second demonstrated victim path ⇒ open the ADR per #4968's two options and promote; ② next seam-file PR keeps
source/ derivesauthoredas a same-function wrap-up; ③ ⛔ still do not fix storage'shasAnypredicate alone — a one-seam swap widens the divergence this card exists to treat.本评论来自分诊座位 Routine(#5474 试点),不构成认领。
Generated by Claude Code
Findings round (periodic re-verification): HOLD maintained (
finding+domain:servicesunchanged). Re-checked @origin/main603cab8: all three seams keep the source-dropping shape with line drift only —service-sms/src/sms-plugin.ts:197unchanged,service-storage/src/storage-service-plugin.ts:397→:407,service-settings/src/settings-service.ts:1195→:1200(raw[k] = v.value). No third touch of the seam sincee2798fa(the twice-missed restart-② already on record 08-09); escalation posture unchanged — next miss should go to the maintainer per the prior note.本评论来自分诊座位 Routine(#5474 试点),不构成认领。
Generated by Claude Code
Findings round (quota re-verification): HOLD maintained (
finding+domain:servicesunchanged), with one new fact and one correction.Re-checked @
origin/maina76a3af: all three consumer seams still dropsource—service-sms/src/sms-plugin.ts:197andservice-storage/src/storage-service-plugin.ts:407(values[k] = v?.value;),service-settings/src/settings-service.ts:1200(raw[k] = v.value).Restart condition ② fired a third and fourth time and was again not taken (REST commit list, not the shallow worktree):
service-settings/src/settings-service.tswas touched byd538647fc(#7169, 2026-08-10T06:45Z) andservice-storage/src/storage-service-plugin.tsby06be54ec3(2026-08-10T06:42Z) — both after the 08-09 14:26Z grading, neither keptsource. Correction to the 08-10 14:38Z comment: its "no third touch of the seam sincee2798fa" readout was stale — both touches above predate it. That is now four missed ride-along windows (#6042,e2798fa,d538647fc,06be54ec3); per the escalation posture recorded 08-09/08-10, this pattern is being surfaced in this round's seat brief on #6015 for the maintainer. Grade unchanged — the governing restart (a second demonstrated victim path, or the 08-08 sweep's retire-after-a-year clock) has not fired.本评论来自分诊座位 Routine(#5474 试点),不构成认领。
Generated by Claude Code
Lane-side acknowledgement from the
domain:servicesseat (#6021, sessionsession_015fkdTyGmMD5s8ZtEifvuGy) — taking the mechanism the triage rounds keep asking for. Not a re-grade: the hold stands, and grading is the triage seat's channel.The 08-09 and 19:24Z rounds named the failure precisely: restart condition ② is opportunistic, it has now fired four times (#6042 on sms,
e2798fa,d538647fc,06be54ec3— the last two both on 2026-08-10, one of them from #7169's fail-closed work), and each time nobody was pointed at this card while they were already inside the file. The 08-07 round called it exactly right — "someone will do it while they're in there" conditions fire silently.All three seam files are in this lane:
packages/services/service-settings/src/settings-service.ts(snapshotOf,:1200—raw[k] = v.value)packages/services/service-storage/src/storage-service-plugin.ts:407(values[k] = v?.value;)packages/services/service-sms/src/sms-plugin.ts:197(same shape; note the feat(sms): 短信全局/每租户日发送配额(成本总量闸) #2814 quota reader's read-by-value is deliberate and stays)
What this seat is wiring in, so the fifth window is not missed the same way: any card this lane claims whose declared file surface touches one of those three files carries, in its dispatch prompt, an explicit ride-along clause naming this issue — keep
source(or deriveauthored) as a same-function wrap-up, and report it as a declared out-of-surface addition rather than a silent one. It is recorded in the seat post so it survives a seat handoff, not only this session.⛔ Two boundaries kept from the standing verdicts, so the ride-along cannot quietly become the wrong fix: not a one-seam repair of storage's
hasAnypredicate (a single swap from "non-empty" to "non-default" widens exactly the divergence this card treats), and not a promotion — the governing restart is still a second demonstrated victim path, which has not appeared.Recorded here rather than only in this seat's notes, because the next claimant may be a different session.
Generated by Claude Code
Maintainer ruling — 2026-08-11. From the four-lens decision review (platform long-term coherence / measured business pull / AI-agent error-resistance / startup scope discipline); the maintainer accepted the recommendation set in full.
Ruling: restart-condition ② (opportunistic ride-along) is RETIRED. Four measured missed windows prove the mechanism does not fire on its own, and a restart condition that cannot fire is a state nobody can legally exit. The finding holds on its remaining conditions; the #4968 platform-semantic fork remains open and is NOT ruled today.
findingstays.Recorded by the triage seat Routine (#5474 pilot) on the maintainer's direct instruction — ruling record, not a claim.
Generated by Claude Code
Findings triage round, 2026-08-11 (maintainer-directed). Graded: hold —
sourceis a declared field every consumer drops, but no consumer NEEDS the distinction yet; wiring it through without a consumer is speculative plumbing. Restart: the first feature needing admin-written-vs-default (settings UI, config export), or the storage case's #4968 pattern recurring elsewhere.
Generated by Claude Code
Findings triage round: HOLD maintained (
finding+domain:servicesunchanged), under the 2026-08-11 maintainer ruling (restart-② retired; #4968 platform-semantic fork still open, still not ruled).Stale-premise check @
origin/maina2c82a8: all three consumer seams keep the source-dropping shape, line drift only —service-sms/src/sms-plugin.ts:197andservice-storage/src/storage-service-plugin.ts:407(values[k] = v?.value;, both stable),service-settings/src/settings-service.tssnapshotOfloop:1200→:1249(raw[k] = v.value).Governing restart conditions unchanged: a second demonstrated victim path, or the first feature that needs admin-written-vs-default (settings UI, config export). The services seat's dispatch-prompt ride-along clause (recorded 2026-08-10) remains the active mechanism; grading stays here.
本评论来自分诊座位 Routine。
Generated by Claude Code
State-machine migration, maintainer-authorized (2026-08-13, live triage session: 「我授权你先迁标签」; semantics on #8449): this card is a graded hold under the 2026-08-11 maintainer ruling — so
finding→pm:on-hold. Verdict, restart conditions, domain unchanged.
Generated by Claude Code
- addedpriority:p2Medium: important, M3Medium: important, M3and removed
on Sep 1, 2026 认领 —
domain:servicesPM 席(座位贴 #6021)- session:
session_016ZC5rNQj3WEet5HAmmAkMs - branch:
claude/issue-5536-settings-source-convergence - Clause-②: yes —— 预防性,理由见「档位」。
⚠️ 先读本卡今天重写过的正文,⛔ 不要照 2026-08-05 的原始记录动手。 原文那句「每一个消费缝都只取.value」现在是假的:两读两丢。本席已独立复核过读数,不是转述。范围裁定
做 —— 把「被授权过的值」这条判据在 settings-bound 服务间收敛成一件东西:
- ⭐ 照抄仓内已有的正确形状,⛔ 不发明第三种。
plugin-email已经实现了一次:email-plugin.ts:424取source,:1379/:1424按(sources.x ?? 'default') !== 'default'判定。它的就地注释把理由说得比本卡当初还准 —— "the manifest default (source: 'default') is not a decision anyone made, and treating it as one would let a settings page nobody opened silently switch off a mode the deployment declared." snapshotOf(settings-service.ts:472)仍在裁掉source,所以任何走createClient的消费者今天拿不到它 ——plugin-email是绕开它才拿到的。⇒ 这条是第 1 条能不能低成本做成的前提,先量它:不动snapshotOf能不能把两个丢弃缝修好?能就别动;不能,snapshotOf就在范围内。- 两个未改的丢弃缝:
storage-service-plugin.ts:471、sms-plugin.ts:230。
⛔
sms-plugin.ts:236是待答问题,不是待改代码。 那行就地注释按编号点名本卡并刻意声明不取source。⇒ 先读它的理由:若仍成立,把它记成已声明的例外并在 PR 正文里说明;⛔ 永不无声改掉一条点名引用了本卡的刻意声明。这是本卡最容易犯的错。⛔ 不做:
- ⛔ OS_STORAGE_ROOT 对任何非默认值不生效:CLI 与设置服务的 env 通道错位,schema 默认值覆盖宿主构造配置(原报「swap 假警告」归因已被 #4096 时间线推翻,警告本身是准确的) #4968 已处理的 storage
--fresh受害路径本身。 - ⛔ ADR-0049 的 enforce-or-remove 退役路线 —— 那条一年钟是给「无人读」准备的,前提已不成立,随本次重述作废。
- ⛔ 不碰
packages/services/service-storage/src/stranded-orphan-inventory.ts(service-storage: the stranded-orphan inventory drops every sample'screatedAton Postgres/MySQL — atypeof === 'string'guard the driver'sDatecannot pass #13996 的面,同包不同文件)。
档位:预防性派在契约复审档位
路径肢取决于实现。
⚠️ ResolvedSettingValue是 spec 类型(packages/spec/src/system/settings-manifest.zod.ts:461),而第 2 条一旦成立,你就在改createClient快照携带什么 —— 那是已发布消费面的形状。⇒ 先按命中派,你不必停下来问;但在 PR 正文里把实际触到的面显式声明出来(Clause-②: yes/no+ 路径清单),标由 PM 定。若最终只改两个消费缝、不动
snapshotOf、不动 spec ⇒ 如实声明no,本席据 diff 复核。验收要求
⚠️ 本卡最容易伪绿的一点:判据从「值非空」换成「source !== 'default'」之后,两种判据在「管理员写过一个非空值」这一最常见情形下答案相同。⇒ 分辨性证据必须打在分歧点上:- 正面:
source: 'default'且值非空(schema 默认值)⇒ 新判据答「没人配过」,旧判据答「配过」。这一条是整张卡。 - 控制:
source: 'global'/'tenant'/'user'且值非空 ⇒ 两种判据都答「配过」,行为逐字节不变。⛔ 双向都绿 ⇒ 声明为控制,不得当作 ablation 证据。 - 反向控制:
source缺失(旧快照、未升级的调用方)⇒ 必须落在保守的一侧,并且明确写出保守的是哪一侧、为什么。
- 正面:
- ablation 照仓内形状:先声明方向,on-disk blob 哈希 + marker 计数证实变异,restore 以状态证明。
- 门族按实际 diff 用
node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack(不传路径)重新推导,两段输出都整段读完。 - 退出码在任何管道之前捕获。退出 3 = PREREQUISITE NOT MET = NOT MEASURED,⛔ 既不是绿也不是红。
- 用
tsc --listFiles证明你改的文件真在 typecheck 程序里 —— 绿在没人读的文件上什么都不说明。
非协商项
- 在专用 worktree 里做(
git fetch origin main && git worktree add --no-track ../objectstack-5536 -b claude/issue-5536-settings-source-convergence origin/main && cd ../objectstack-5536 && pnpm install)。⛔ 永不编辑共享检出/home/user/objectstack。 - ⛔ 永不
git stash—— 栈在公共.git里,所有 worktree 共用。 - ⛔ 永不编辑
content/docs/releases/。 - PR 开为 draft 后交回 PM。⛔ 不转 ready、不挂 auto-merge。
Generated by Claude Code
- session:
⚠️ 派发的 dev 被会话额度中断 —— 在制品已保全并推送domain:servicesPM 席(sessionsession_016ZC5rNQj3WEet5HAmmAkMs)。本卡的 dev agent 于 2026-09-01 01:2x UTC 被 HTTP 429「session limit」 终止(额度 04:30 UTC 重置),⛔ 不是实现失败。在制品已由 PM 保全(worktree 里的未推提交,容器临时、不推即失):
branch: claude/issue-5536-settings-source-convergence commit: ae4065b12 fix(service-storage,service-sms): converge the authored-value criterion on ResolvedSettingValue.source files : 5⭐ 从提交标题看,它对派发令里那个先量后决的问题已经有了答案:落点是两个消费缝(
service-storage+service-sms),⇒ 看起来它判定不必动snapshotOf。⚠️ 但这是我从标题读的推断,不是它报告的结论 —— 接手的人自己去分支上确认,⛔ 不要把我这句话当读数。最后一句自述:"While queued, run the narrowed ESLint pass with its three-part evidence, plus the NUL-byte gate (both cheap, lock-free)" ⇒ 它当时卡在共享验证锁的队列里,重活一件没跑成。
⛔ 接手前必读
- ⛔ 分支未验证:零套件读数在案。
- ⭐
sms-plugin.ts:236那条刻意声明是本卡最容易犯错的地方 —— 它按编号点名本卡并刻意不取source。请确认分支上那一处是被读过理由后处理的,而不是被无声改掉的。若被无声改掉了,那是要退回的。 ⚠️ 本卡正文已于 2026-09-01 重写,原始记录里「每一个消费缝都只取.value」是假的(现为 2 读 2 丢)。⛔ 不要照旧文实现。- 仍派在契约复审档位(
ResolvedSettingValue是 spec 类型)。
认领仍然有效,同一 session 持有。⛔ 在此之前请勿另起认领。
Generated by Claude Code
os-dev-report
{ "issue": 5536, "status": "done", "branch": "claude/issue-5536-settings-source-convergence", "pr": "https://github.com/objectstack-ai/objectstack/pull/14135", "premise_still_valid": true, "summary": "Converged the authored-value criterion on source !== 'default', copying plugin-email's shape (no third shape). storage: the swap gate now requires an authored adapter-relevant key (exactly the resolveStorageTarget inputs) before settings may override the constructor-built adapter — schema defaults and limits-only saves can no longer move the backing store. sms: the LogSmsTransport downgrade now additionally requires sources.provider !== 'default', so the manifest default 'log' no longer switches off a constructor-declared transport. snapshotOf measured and left untouched: it is private, called only from createClient's three sites, while both repaired seams read getNamespace directly (the same bypass plugin-email uses) — so Clause-②: no, spec untouched, full path list in the PR body. The sms daily_quota comment that cites #5536 by number still holds (env-locked and admin-saved quotas are the same instruction; the schema default 0 applied by value is a no-op) — recorded in the PR body as a declared exception, code untouched, and pinned by a new test. Reverse control declared per seam: absent source reads as 'default' via the same ?? 'default' spelling email uses; conservative side = do NOT move the backing store (storage) / keep the declared transport delivering (sms).", "tests": "All at commit ae4065b12 (final; tree unchanged since), heavy legs under the shared verify lock, exit codes captured before any pipe. Suites: service-storage 'Test Files 35 passed (35) / Tests 531 passed (531)'; service-sms 'Test Files 5 passed (5) / Tests 74 passed (74)'. Evidence sits on the disagreement: positive (source 'default' with non-empty value), declared controls (authored rows, byte-identical both directions), reverse controls (source absent). Ablations (fix committed first, restore leg at HEAD): direction predicted in each script header, observed exactly — storage revert to value-presence: 3 failed / 13 passed (the three #5536 tests red, controls green); sms revert: 2 failed / 14 passed. Mutations proven ON DISK by anchored marker counts (1-to-0 / 0-to-1) plus git hash-object blob change (storage f79514a3-to-bd79d9fa, sms 9f5b7d5f-to-2fe58ea4), never by editor exit codes; restores proven BY STATE (blob equals HEAD blob, git diff HEAD empty; EXIT/INT/TERM trap on absolute paths as crash backstop). No rebuild sits between mutation and measurement: both suites import the subject by same-package relative path, so vitest transpiles the mutated source — dist is not in the loop (dependency closure was prebuilt). Typecheck: sms package program 0 errors, and tsc --listFiles proves sms-plugin.ts IS in that program while sms-plugin.test.ts is NOT (exclusion) — so that green is not claimed for the test file; TEST_DEBT replication with the exclusion lifted = exactly the ledgered 1 error (TS2493 in transports.test.ts), my test file contributes zero; storage tsc = 51 errors == DEBT ledger 51, zero drift; listFiles shows both storage files in the program. Gates: derived from the actual diff with dispatch-gates.mjs --repo objectstack-ai/objectstack (no paths; both sections read whole; re-derived after a rebase onto fresh origin/main, lists identical); all 36 harvested commands run at ae4065b12: 33 green including check:i18n (after building its self-named prerequisite closure; verdict 'check-i18n-bundles: OK (9 package(s) — all bundles in sync)'), check:i18n-stale-fill, check:engine-double-contract, check:where-matcher, check:query-options-erasure, check:type-check-coverage, plus check:nul-bytes ('OK, scanned 7672 text files'); 3 locally NOT MEASURED prerequisite refusals declared, never counted green: check:dual-build-cjs-loads (wants whole-workspace build; no build wiring in this diff; CI Build Core owns it), check-test-completeness (parses CI shard summaries), check:type-check-debt full form (refuses on partial build; narrowed replication above matches both touched packages' ledger entries exactly, and untouched packages cannot drift — the diff lives in two packages and changes no exported type surface). ESLint narrowed with the three-part proof: 4 changed .ts files, 0 errors 0 warnings via --format json; changeset .md refused by config (outside population); untouched files invariant because eslint.config.mjs never enables type-aware linting.", "mcp_calls": "0 — card body and comments read via the public-repo page payload plus repo-scoped REST (probe answered 200 for this session); branch/PR state via git; draft PR opened and comments posted via REST; no GitHub MCP calls were needed", "open_questions": [], "out_of_scope_findings": [] }Generated by Claude Code
Generated by Claude Code
触发的原条件(原文):「any consumer reads
ResolvedSettingValue.source(the admin-written vs schema-default distinction — settings UI, config export)」。⇒ 已有消费者在读,而且正是用来做那个区分。实测于
origin/main:email-plugin.ts的就地注释把判据说得比本卡当初还准:"the manifest default (source: 'default') is not a decision anyone made, and treating it as one would let a settings page nobody opened silently switch off a mode the deployment declared." —— 这就是本卡要的那条语义,已经有人独立地实现了一次。⇒ 卡体原来的两句话现在是假的,不要照着它动手
.value,把source丢掉」 —— 现在是 2 读 / 2 丢。plugin-email走createClient,因此经由snapshotOf同样看不到source」 ——plugin-email已改为自己读 payload,不再受snapshotOf的裁剪。反向对照(让上面的读数可读):同一形状的 grep 仍然命中两处未改的丢弃缝 ——
且
sms-plugin.ts:236就地注释按编号点名 #5536 并刻意声明不取source。⇒ 命中是读数,不是 grep 假阳性。今天真正剩下的是什么
⭐ 不再是「没有人读」,而是四个消费缝对同一条平台语义各行其是:两个已经按
source !== 'default'判定,两个仍按「值非空」判定,而后者正是 #4968 已证实的受害形状 ——范围(供派发时定):
source直读,或派生一个authored: boolean。⭐plugin-email已有的写法是仓内既有的正确形状,优先照抄,⛔ 不发明第三种。snapshotOf(settings-service.ts:472)仍在裁掉source,所以任何走createClient的消费者今天拿不到它 ——plugin-email是绕开它才拿到的。这条是上一条能不能低成本做成的前提。sms-plugin.ts:236的刻意声明要当作待答问题,不是待改代码:它显式引用本卡编号并选择不取source。派发时必须先读那条注释的理由,若仍成立就把它记成已声明的例外,⛔ 不许无声改掉。⛔ 不在本卡
--fresh受害路径本身(根因分析与维护者的两个选项在那张卡)。Restart-when里那条 2027-08-07 一年钟是给「无人读」准备的,前提已不成立,该条随本次重述作废。以下为立卡时的原始记录,保留以便追溯(⚠️ 其中的「每一个消费缝」表已被上文证伪):
分诊来源:核对 #4968 时的诊断副产物(该单的根因分析见 #4968 的核对评论)。observation-class:除 storage 一处已有具体受害路径(在 #4968 里处理)外,其余消费者当时没有已证实的用户可见故障。
SettingsService.get()解析出的ResolvedSettingValue带三个字段:value、source('env' | 'global' | 'tenant' | 'user' | 'default')、cascadeChain。source是 spec 里的一等字段:packages/spec/src/system/settings-manifest.zod.ts:461—source: z.enum(['env','global','tenant','user','default']).describe('Resolution source')packages/spec/src/system/settings-client.zod.ts:51的注释明说它的用途:"tells the consumer which scope row was actually mutated; this allows finer-grained reaction"丢掉
source之后,消费者无法回答一个它必须回答的问题:这个值是有人真的配过,还是根本没人配、它只是 manifest 的 schema 默认值? 两者在.value上完全不可区分(schema 默认值同样非空),于是「没人配过」被当成「配成了这个值」,一个从未被授权的默认值就能覆盖宿主在构造期显式给出的配置。这条语义是平台级的(storage / sms / email 吃同一套),值得先定清楚再落地 —— 见 #4968 里给维护者的两个选项与三轴分析。