分诊来源:核对 #4968 时的诊断副产物(该单的根因分析见 #4968 的核对评论)。observation-class:除 storage 一处已有具体受害路径(在 #4968 里处理)外,其余消费者今天没有已证实的用户可见故障,故打 finding、不进 pm:queue,留给 PM 分诊定级。
事实
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"
但每一个消费缝都只取 .value,把 source 丢掉:
| 位置 |
代码 |
packages/services/service-settings/src/settings-service.ts:472 |
for (const [k, v] of Object.entries(payload.values)) raw[k] = v.value;(snapshotOf,即 createClient 快照的来源) |
packages/services/service-storage/src/storage-service-plugin.ts:397 |
values[k] = v?.value; |
packages/services/service-sms/src/sms-plugin.ts:157 |
values[k] = v?.value; |
plugin-email(packages/plugins/plugin-email/src/email-plugin.ts:306)走 createClient,因此经由 snapshotOf 同样看不到 source。
为什么这是缺陷,而不只是一个没用到的字段
丢掉 source 之后,消费者无法回答一个它必须回答的问题:这个值是有人真的配过,还是根本没人配、它只是 manifest 的 schema 默认值?
两者在 .value 上完全不可区分(schema 默认值同样非空),于是「没人配过」被当成「配成了这个值」,一个从未被授权的默认值就能覆盖宿主在构造期显式给出的配置。storage 是已证实的受害路径:--fresh 传入的 uploads 根目录被 storage.local_root 的 schema 默认值顶掉(实测证据与完整链路在 #4968)。
同一形状的判据在 storage 里被写成了「值非空」:
const hasAny = Object.values(values).some((v) => v !== undefined && v !== null && v !== '');
if (!hasAny) return;
紧邻的注释写的却是 "No persisted values yet → keep the constructor-built adapter" —— 注释说 persisted,代码测 present。它真正想表达的判据是 source !== 'default',而这个信息在上面三行就被丢掉了。
建议方向(不在本单实施)
消费缝保留 source(或至少派生一个 authored: boolean),让 settings-bound 服务能按「被授权过的值」而不是「非空的值」做决策。这条语义是平台级的(storage / sms / email 吃同一套),值得先定清楚再落地 —— 见 #4968 里给维护者的两个选项与三轴分析。
分诊来源:核对 #4968 时的诊断副产物(该单的根因分析见 #4968 的核对评论)。observation-class:除 storage 一处已有具体受害路径(在 #4968 里处理)外,其余消费者今天没有已证实的用户可见故障,故打
finding、不进pm:queue,留给 PM 分诊定级。事实
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"但每一个消费缝都只取
.value,把source丢掉:packages/services/service-settings/src/settings-service.ts:472for (const [k, v] of Object.entries(payload.values)) raw[k] = v.value;(snapshotOf,即createClient快照的来源)packages/services/service-storage/src/storage-service-plugin.ts:397values[k] = v?.value;packages/services/service-sms/src/sms-plugin.ts:157values[k] = v?.value;plugin-email(packages/plugins/plugin-email/src/email-plugin.ts:306)走createClient,因此经由snapshotOf同样看不到source。为什么这是缺陷,而不只是一个没用到的字段
丢掉
source之后,消费者无法回答一个它必须回答的问题:这个值是有人真的配过,还是根本没人配、它只是 manifest 的 schema 默认值?两者在
.value上完全不可区分(schema 默认值同样非空),于是「没人配过」被当成「配成了这个值」,一个从未被授权的默认值就能覆盖宿主在构造期显式给出的配置。storage 是已证实的受害路径:--fresh传入的 uploads 根目录被storage.local_root的 schema 默认值顶掉(实测证据与完整链路在 #4968)。同一形状的判据在 storage 里被写成了「值非空」:
紧邻的注释写的却是 "No persisted values yet → keep the constructor-built adapter" —— 注释说 persisted,代码测 present。它真正想表达的判据是
source !== 'default',而这个信息在上面三行就被丢掉了。建议方向(不在本单实施)
消费缝保留
source(或至少派生一个authored: boolean),让 settings-bound 服务能按「被授权过的值」而不是「非空的值」做决策。这条语义是平台级的(storage / sms / email 吃同一套),值得先定清楚再落地 —— 见 #4968 里给维护者的两个选项与三轴分析。