发现于 #5005 实现过程,与本单修复无关(#5005 的主题是被丢掉的键,i18n 没被丢),按 Prime Directive #10 单独立单,未认领。
事实
#5005 落地后,composeStacks 的非数组顶层键统一为同值放行 / 冲突报错。唯一的例外是 i18n:
// packages/spec/src/stack.zod.ts — composeStacks 第 2 步
// 2. i18n — last-wins (single object, not array). Pre-existing strategy,
// deliberately untouched by #5005; see COMPOSE_KEY_DISPOSITIONS.
for (let i = stacks.length - 1; i >= 0; i--) {
if (stacks[i].i18n) {
composed.i18n = stacks[i].i18n;
break;
}
}
实测(#5005 的控制用例钉住了现状):
const a = defineStack({ manifest: A, i18n: { defaultLocale: 'en' } });
const b = defineStack({ manifest: B, i18n: { defaultLocale: 'zh-CN' } });
composeStacks([a, b]).i18n // → { defaultLocale: 'zh-CN' } —— a 的声明无声消失
为什么值得单独看一眼
维护者 2026-08-04 在 #5005 上裁决 ⛔ 不做 last-wins,理由是「静默安全降级 —— 先声明的栈被后组合者无声覆盖」。i18n 就是字面意义上的 last-wins:一个装了 CRM(defaultLocale: 'zh-CN')和某个附加包(defaultLocale: 'en')的组合栈,谁在数组里靠后谁定语言,作者不会收到任何提示。
后果比 api / server 轻(是本地化而非授权),这也是 #5005 明确把它留在范围外的原因 —— 它是这里唯一本来就有明确、可工作策略的键,改它会打断今天真在依赖它的组合,而 #5005 修的是「键消失」。但改完之后它成了整个顶层键面上唯一的不一致,值得单独定一次。
待裁决(不要猜)
- A. 与其余非数组键对齐 —— 同值放行、冲突报错带处方。一致、响亮;代价是今天靠 last-wins 覆盖语言配置的组合会开始报错(仓内实测无真实调用点,外部未知)。
- B. 保留 last-wins,但冲突时 warn 一次并点名 —— 不破坏现有组合,把静默变成有声。比 A 弱,但零破坏。
- C. 维持现状 —— 认定本地化配置的后者优先是刻意语义,并把这个理由写进 docblock,免得下一个人再提一次。
ComposeStacksOptionsSchema 上不建议为此加旋钮:#5005 已裁决显式覆盖机制留给定制故事真拉动时统一设计,单给 i18n 开一个口子会预支那个设计。
关联:#5005(合成语义裁决与实现)、#4910。
发现于 #5005 实现过程,与本单修复无关(#5005 的主题是被丢掉的键,
i18n没被丢),按 Prime Directive #10 单独立单,未认领。事实
#5005 落地后,
composeStacks的非数组顶层键统一为同值放行 / 冲突报错。唯一的例外是i18n:实测(#5005 的控制用例钉住了现状):
为什么值得单独看一眼
维护者 2026-08-04 在 #5005 上裁决 ⛔ 不做 last-wins,理由是「静默安全降级 —— 先声明的栈被后组合者无声覆盖」。
i18n就是字面意义上的 last-wins:一个装了 CRM(defaultLocale: 'zh-CN')和某个附加包(defaultLocale: 'en')的组合栈,谁在数组里靠后谁定语言,作者不会收到任何提示。后果比
api/server轻(是本地化而非授权),这也是 #5005 明确把它留在范围外的原因 —— 它是这里唯一本来就有明确、可工作策略的键,改它会打断今天真在依赖它的组合,而 #5005 修的是「键消失」。但改完之后它成了整个顶层键面上唯一的不一致,值得单独定一次。待裁决(不要猜)
ComposeStacksOptionsSchema上不建议为此加旋钮:#5005 已裁决显式覆盖机制留给定制故事真拉动时统一设计,单给i18n开一个口子会预支那个设计。关联:#5005(合成语义裁决与实现)、#4910。