fix(spec)!: composeStacks 不再静默丢弃顶层键 —— 同值放行 / 冲突报错 / 未声明必警 (#5005) - #5053
Merged
xuyushun441-sys merged 2 commits intoAug 4, 2026
Merged
Conversation
) composeStacks built its result from an empty object, filling in manifest, i18n, objects and a hand-maintained array whitelist. Anything absent from that whitelist was DELETED — no error, no warning, and `undefined` at the consumer is indistinguishable from "the author never wrote it". Composition is the platform's app-packaging/install story, so the silence reached security config: `api.enforceProjectMembership` (the per-environment 403 gate) and `server.security.rateLimit` (#4910) both vanished the moment a stack was composed with any other one, as did `functions` (every declarative handler), seven declared array collections (datasourceMapping, datasets, jobs, emailTemplates, docs, books, tiers) and `runtimeModule`. Per the 2026-08-04 maintainer verdict: 1. same value in several stacks composes fine (deep equality); 2. differing values THROW, naming the key, both source stacks and the two ways out — NOT last-wins (a silent security downgrade: an add-on package switching off an earlier stack's 403 gate) and NOT deep-merge (a third value neither author wrote); 3. a top-level key with no declared composition rule WARNS and is composed by the default, so the next new key reports itself instead of being found by accident the way `server:` was. Array keys keep their concat semantics unchanged. `functions` merges by name (composing CRM + Todo must yield both packages' handlers) and throws on a duplicate name; the map and array forms are merged in kind, never converted (an array entry carries `packageId` the map entry cannot hold). `i18n` keeps its pre-existing last-wins — it is the one key here that already had a working strategy, and #5005's subject is keys that were dropped. The whitelist made forgetting the default; the replacement is a total disposition table typed `Record< keyof ObjectStackDefinition, ... >`, so a new top-level key does not compile until someone states what composing it means. The runtime warn covers what the type cannot see (`strict: false`, hand-built stack objects). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ehu85kbvMcrNTUJjwxvLJ9
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
Contributor
📓 Docs Drift CheckThis PR changes 1 package(s): 107 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
|
…elpers The warn-helper insertion left `warnUncomposedStackKey`'s docblock attached to `warnedMalformedCollectionKeys`. Comment-only; no behaviour change. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ehu85kbvMcrNTUJjwxvLJ9
xuyushun441-sys
marked this pull request as ready for review
August 4, 2026 01:30
xuyushun441-sys
deleted the
claude/issue-5005-compose-stacks-merge-semantics
branch
August 4, 2026 01:46
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #5005
按维护者 2026-08-04 在 #5005 上的裁决实现:同值放行、冲突报错带处方、未声明规则的顶层键必警。⛔ 不做 last-wins、⛔ 不做 deep-merge、⛔ 不预支显式覆盖机制。
定位更正
派单假设
composeStacks在 engine 侧(objectql/runtime),实测不是:它在packages/spec/src/stack.zod.ts:1413,仓内除packages/spec/src/index.ts的 re-export 外零调用点。所以本 PR 落在packages/spec源上 —— 但没有动任何 Zod schema:ObjectStackDefinitionSchema/ComposeStacksOptionsSchema一字未改,check:authorable-surface实测零漂移,新增符号全部不导出(api-surface.json不受影响)。改的只是那个函数的行为。病灶
composeStacks从空对象{}开始逐项填充:manifest、i18n、objects,再加一份手工维护的数组白名单CONCAT_ARRAY_FIELDS。不在白名单里的顶层键不是「原样保留」,而是被删除 —— 不报错、不告警,消费方拿到的undefined与「作者从没写过」无法区分。stacks.length === 1时函数原样返回,所以单栈一切正常,只有真正 ≥2 个栈才丢 —— 这是它至今没被发现的原因。ADR-0109 当年也只是给tools补了一行白名单,并没有堵住这一类。顶层键逐项处置(修复前 → 修复后)
ObjectStackDefinitionSchema今天声明 42 个顶层键,全部列在下面。修复前会被丢掉的(11 个)
apiobjectstack serve→ REST + dispatcher(含enforceProjectMembership每环境成员 403 闸门)serverobjectstack serve→ 入站限流器(security.rateLimit/trustProxy,#4910)runtimeModulefunctionsAppPlugin启动绑定,声明式 hook / action / script 节点按名解析datasourceMappingdatasetsjobsIJobService定时任务emailTemplatesIEmailService.sendTemplatedocsbookstiersdatasourceMapping正是 issue 里那条「它是数组,只是没被列进CONCAT_ARRAY_FIELDS(需实测确认)」—— 实测确认成立,另外 6 个数组键同病。行为不变的(31 个)
manifest—— 仍按manifest选项择一('first'/'last'/ 索引)。objects—— 仍按objectConflict策略(error/override/merge)。i18n—— 保留既有 last-wins。它是这里唯一本来就有明确、可工作策略的键,而本单主题是「被丢掉的键」,改它会打断今天真在依赖它的组合。改完后它成了顶层键面上唯一的不一致,已单独立单:composeStacks的i18n仍是 last-wins —— #5005 裁决否掉的那个形状,只剩这一个键还在用 #5051(未认领,附 A/B/C 三个选项)。datasourcestranslationsobjectExtensionsappsviewspagesdashboardsreportsactionsthemesflowspositionspermissionscapabilitiessharingRulesapiswebhooksagentstoolsskillshooksmappingsanalyticsCubesconnectorsdatapluginsrequiresdevPlugins—— 拼接语义一字不变,由控制用例钉住。三条规则
同值放行 —— 多个栈声明同一个单值顶层键且深相等,照常合成(
undefined视同未声明)。冲突报错,信息点名冲突键、两个来源栈(manifest id,无 manifest 时退化为
stack #N)与两条出路:未声明规则的顶层键必警 —— 按默认规则合成(数组拼接,其余按单值规则)并点名告警指向
composeStackssilently drops every non-array top-level key —api:today,server:as of #4910 #5005,而不是消失。同一个键 warn 一次,与本模块其它 authoring-time 提示同姿态。functions单独说明:它是命名集合,不是不透明配置块 —— 组合 CRM + Todo 必须两边的 handler 都在,否则每个按名解析 handler 的声明式 hook / action / script 节点在启动时全断。所以按名合并,重名报错(与objectConflict: 'error'一致)。map 与 array 两种书写形态各自同形合并,不互转(array 条目带packageId,map 条目没有位置放它,转换会丢 provenance),混用报错并指示统一形态。结构性保证:白名单 → 全量处置表
白名单让「忘记」成为默认。替换成的处置表类型是:
新增一个顶层键而没说清它怎么合成,
tsc --noEmit直接不过。实测(临时删掉server: 'single'一行):CONCAT_ARRAY_FIELDS现在从这张表派生,两者不可能再漂移。运行时那条 warn 兜住类型看不见的入口(strict: false、手搓 stack 对象)。一个补齐的边角:声明为集合的键却携带非数组值时,过去也是静默跳过 —— 同一个缺陷的缩小版,现在同样 warn(仅
strict: false/ 手搓对象可达,strictdefineStack在书写处就拒绝)。爆炸半径
composeStacks除packages/spec/src/index.ts的 re-export 外没有任何调用点(examples / platform apps / tests 之外)—— 实测grep -rn composeStacks只命中文档(content/docs/getting-started/examples.mdx、skills/objectstack-platform/SKILL.md)、ADR-0109 与 CHANGELOG。没有任何真实组合依赖旧的静默行为,因此没有需要顺带修正的组合。packages/spec/src/compose-stacks.test.ts44 例全绿未改一行。check:authorable-surface通过;新增符号全部不导出,api-surface.json不变(check:api-surface在本 worktree 因dist/未构建而无法运行,已用「新增行零export」实测替代)。测试
RED-first。新用例文件
packages/spec/src/compose-stacks-key-loss.test.ts先在未改动的 origin/main 形状代码上跑,复现 issue 自带的证据:实现后:
覆盖:issue 原始复现(
api/server单侧声明存活,且与顺序无关)、同值深相等放行且不 warn、冲突报错点名键 + 两个栈 + 处方、非 last-wins(两个方向都报错)、非 deep-merge(不相交子键仍报错)、无 manifest 时退化为stack #N、未知键 warn 且仍被合成、未知数组键默认拼接、集合键携非数组值 warn、数组拼接控制用例、7 个曾被丢弃的数组键、functions按名合并与重名报错、manifest/objects/i18n三个既有策略的控制用例,以及一条结构钉:声明了每一个 schema 顶层键的栈组合后零丢失且零告警(有新键没接进来时,它自己在这里报出名字)。Changeset
.changeset/compose-stacks-no-silent-key-loss.md——@objectstack/spec: major。破坏性在于:组合两个对api/server/runtimeModule声明了不同值的栈,过去静默丢弃、现在抛错(functions重名同理)。这正是要的 —— 过去那次「成功」的组合,产出的是一个少了 403 闸门或少了 handler 的栈。v17 窗口开着(.changeset/pre.json实测mode: pre/tag: rc),与仓内在飞的 72 份@objectstack/spec: majorchangeset 同惯例。https://claude.ai/code/session_01Ehu85kbvMcrNTUJjwxvLJ9
Generated by Claude Code