Skip to content

settings 消费缝丢弃 ResolvedSettingValue.source —— 服务无法区分「管理员写过的值」和「schema 默认值」 #5536

Description

@os-zhuang

⚠️ 本卡 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." —— 这就是本卡要的那条语义,已经有人独立地实现了一次。

⇒ 卡体原来的两句话现在是的,不要照着它动手

  1. ❌ 「每一个消费缝都只取 .value,把 source 丢掉」 —— 现在是 2 读 / 2 丢
  2. ❌ 「plugin-emailcreateClient,因此经由 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

范围(供派发时定):

  1. 把「被授权过的值」判据在 settings-bound 服务间收敛成一件东西 —— source 直读,或派生一个 authored: boolean。⭐ plugin-email 已有的写法是仓内既有的正确形状,优先照抄,⛔ 不发明第三种
  2. snapshotOf(settings-service.ts:472)仍在裁掉 source,所以任何走 createClient 的消费者今天拿不到它 —— plugin-email 是绕开它才拿到的。这条是上一条能不能低成本做成的前提。
  3. sms-plugin.ts:236 的刻意声明要当作待答问题,不是待改代码:它显式引用本卡编号并选择不取 source。派发时必须先读那条注释的理由,若仍成立就把它记成已声明的例外,⛔ 不许无声改掉。

⛔ 不在本卡


以下为立卡时的原始记录,保留以便追溯(⚠️ 其中的「每一个消费缝」表已被上文证伪):

分诊来源:核对 #4968 时的诊断副产物(该单的根因分析见 #4968 的核对评论)。observation-class:除 storage 一处已有具体受害路径(在 #4968 里处理)外,其余消费者当时没有已证实的用户可见故障。

SettingsService.get() 解析出的 ResolvedSettingValue 带三个字段:valuesource('env' | 'global' | 'tenant' | 'user' | 'default')、cascadeChainsource 是 spec 里的一等字段:

  • packages/spec/src/system/settings-manifest.zod.ts:461source: 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 里给维护者的两个选项与三轴分析。

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions