⚠️ 本卡 2026-09-01 从 pm:on-hold 解除并重述范围。 原 Restart-when 已触发,且卡体原来的中心事实已被证伪。下面的正文已按今天的树重写;历史读数保留在评论里。
触发的原条件(原文):「any consumer reads ResolvedSettingValue.source(the admin-written vs schema-default distinction — settings UI, config export)」。
⇒ 已有消费者在读,而且正是用来做那个区分。实测于 origin/main:
packages/plugins/plugin-email/src/email-plugin.ts:424 if (v?.source) sources[k] = String(v.source);
packages/plugins/plugin-email/src/email-plugin.ts:1379 if ((sources.queue_delivery ?? 'default') !== 'default') {
packages/plugins/plugin-email/src/email-plugin.ts:1424 const selected = (sources.provider ?? 'default') !== 'default';
packages/services/service-settings/src/settings-service.ts:2154
if (v.source === 'global' || v.source === 'tenant' || v.source === 'user') {
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 仍然命中两处未改的丢弃缝 ——
packages/services/service-storage/src/storage-service-plugin.ts:471 values[k] = v?.value;
packages/services/service-sms/src/sms-plugin.ts:230 values[k] = v?.value;
且 sms-plugin.ts:236 就地注释按编号点名 #5536 并刻意声明不取 source。⇒ 命中是读数,不是 grep 假阳性。
今天真正剩下的是什么
⭐ 不再是「没有人读」,而是四个消费缝对同一条平台语义各行其是:两个已经按 source !== 'default' 判定,两个仍按「值非空」判定,而后者正是 #4968 已证实的受害形状 ——
const hasAny = Object.values(values).some((v) => v !== undefined && v !== null && v !== '');
if (!hasAny) return; // 注释写 "No persisted values yet",代码测的却是 present
范围(供派发时定):
- 把「被授权过的值」判据在 settings-bound 服务间收敛成一件东西 ——
source 直读,或派生一个 authored: boolean。⭐ plugin-email 已有的写法是仓内既有的正确形状,优先照抄,⛔ 不发明第三种。
snapshotOf(settings-service.ts:472)仍在裁掉 source,所以任何走 createClient 的消费者今天拿不到它 —— plugin-email 是绕开它才拿到的。这条是上一条能不能低成本做成的前提。
- ⛔
sms-plugin.ts:236 的刻意声明要当作待答问题,不是待改代码:它显式引用本卡编号并选择不取 source。派发时必须先读那条注释的理由,若仍成立就把它记成已声明的例外,⛔ 不许无声改掉。
⛔ 不在本卡
以下为立卡时的原始记录,保留以便追溯(⚠️ 其中的「每一个消费缝」表已被上文证伪):
分诊来源:核对 #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 里给维护者的两个选项与三轴分析。
触发的原条件(原文):「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 里给维护者的两个选项与三轴分析。