Found while implementing #14439 (the ADR-0130 D4 producer). Filed rather than solved there: removing either copy changes what a booted instance can see, which is a design decision, not a compiler's.
The fact
composeStacks([...], { manifest: 'preserve' }) is deliberately additive: it produces the flattened stack the platform has always produced pluspackages[], whose entries are the same metadata assembled per package. So a compiled multi-package artifact carries every object, view, flow and permission set twice.
Measured on examples/app-multi-package (two packages, one object each): dist/objectstack.json is 7.2 KB, of which the packages[] half is a near-copy of the top-level half. The ratio does not improve with size — it is one extra copy of everything a package owns.
Why both copies exist today
They feed two different readers, and each reader ignores the other's copy:
- The flattened top level is what
MetadataPlugin._parseAndRegisterArtifact iterates (ARTIFACT_FIELD_TO_TYPE over the parsed definition, packages/metadata/src/plugin.ts). It never looks at packages. Drop the top level and a booted instance has no views, flows, permission sets or datasets in the metadata service at all. packages[] is what ObjectQL.registerApp iterates, one manifest at a time, and it is the only thing that can stamp per-package ownership (_packageId, the namespace-owner set). Drop it and the artifact installs N package records owning nothing — which is the state ADR-0130 D4 existed to leave behind.
D4's read-both rule is what makes the second point sharp: when packages is present the load path registers through it, so the top-level collections reach registerApp not at all.
Why it matters
ADR-0130 D4 reserves the { ref, integrity } external-segment position specifically because artifact size is a real constraint — 黑猫's single-package artifact is already 2.6 MB, and both marketplace transfer and startup parse scale with it. Doubling the payload is the opposite direction, and it arrives at the same moment the format gains the key that was meant to help.
There is a correctness edge too, though nothing exercises it today: two copies of one definition can be edited apart. Nothing reconciles them, and which one a consumer sees depends on which reader it went through.
What the decision is
- A. Leave it. The duplication is honest (both readers are real) and the cost is bounded by the artifact staying one file. Revisit when a real artifact gets large.
- B. Teach the metadata artifact door to read
packages[] — iterate the package bodies when the key is present, exactly as the ObjectQL side does — and stop emitting the flattened collections for multi-package artifacts. Single-package artifacts keep today's shape untouched (D4/D7). This is the one that removes the copy rather than compressing it. - C. Keep both readers but make the artifact carry each definition once, with
packages[i] referencing top-level items by name. Cheapest on bytes, and the one that most deserves scrutiny: a reference that resolves at load is a second lookup path for metadata identity, and a dangling one fails silently.
Not urgent: no customer artifact carries packages[] yet, so this can be decided before it is expensive rather than after.
Triage — graded
Located at origin/main4a37870: composeStacks is packages/spec/src/stack.zod.ts:2451; the metadata-side reader is packages/metadata/src/plugin.ts:72 (ARTIFACT_FIELD_TO_TYPE) consumed at :935. The artifact shape is a packages/spec contract, which is what sets the lane.
The card needs no correction — it states its own three options fairly, names the correctness edge without overclaiming it ("nothing exercises it today"), and is explicit that no customer artifact carries the key yet. Graded as a decision because every option changes the compiled artifact format, which is a published contract with an ADR (ADR-0130 D4/D7) on it.
<!-- os-decision-facets -->
- ① 项目长远合理性(权重 ≥50%,领起推荐) —— 一份定义存两份、两个读取方各读各的,是标准的双真相来源。今天两份一致纯粹因为它们由同一次编译同时生成 —— 没有任何东西在核对它们。而 ADR-0130 D4 专门留出「外部分段引用」的位置,理由正是产物体积是真实约束;在这个当口把载荷翻倍,方向恰好相反。①指向「一份定义只存一次」,并且在 B 与 C 之间明确偏 B:C 引入的是「按名字引用」这条新的元数据身份解析路径 —— 那是新增特例,不是缩小特例。
- ② 实际业务拉动 —— 今天为零,卡面自己说得很清楚:没有任何客户产物带
packages[]。⚠️ 但零拉动在这里的读法要反过来:产物格式一旦有客户落地就固化了。现在改是零迁移面,以后改是破坏性格式变更。 所以「零拉动」是「趁现在便宜」的理由,不是「先放着」的理由 —— 这是本卡与一般零拉动卡最大的不同。 - ③ 防 AI 犯错 —— 出错时谁看到什么:两份副本被改岔,消费者看到哪一份取决于它走了哪个读取方。不报错,只是两个不一致的答案。 卡面说今天没有东西会造成分歧,这是对的;但「一份定义两处存储」本身就是等着分歧的结构。B 让这种分歧在结构上不可能;C 把它换成「悬空引用静默失败」—— 卡面自己就点了这一条,是三个方案里最该被审视的。
- ④ 创业阶段不扩散 —— 每一种写进产物格式的形状都是永久义务:向后兼容、迁移工具、文档。
packages[] 目前没有任何客户产物,这是唯一一次可以零成本收敛的窗口,过了就得带着两份副本做兼容。
推荐:A = 卡面的方案 B。 教元数据产物读取方在 packages[] 存在时按包体遍历,多包产物不再输出扁平集合;单包产物形状原样不动(D4/D7)。①领起 —— 它删掉一份,而不是新增一条查找路径;③让改岔在结构上不可能;④不新增格式概念;②的零拉动不反对它,反而正是它现在做最便宜的理由。
回退:B' = 卡面的方案 A(先不动)。 若维护者认为 D4 的外部分段位置本就是为体积准备的、届时一并处理更省事,走这条 —— 代价写明:届时已有客户产物,同一个变更从「零迁移」变成「破坏性」。
⛔ 不荐方案 C。 按名字引用会新增第二条元数据身份解析路径,悬空时静默失败,③④都反对;卡面把它列为「最该被审视的一个」,本席同意并且把它排在最后。
置信缺口(本分析看不见什么): 没有量方案 B 的真实成本。卡面说 MetadataPlugin._parseAndRegisterArtifact 今天完全不看 packages 键,但没有说它要读的那些集合(views / flows / permission sets / datasets)在包体里的形状是否与顶层一致。若不一致,B 就不是「换个地方遍历」而是一次形状适配,成本和推荐都要重排。这是执行 B 之前必须先量的第一件事,不是执行中顺手确认的事。
Generated by Claude Code
Found while implementing #14439 (the ADR-0130 D4 producer). Filed rather than solved there: removing either copy changes what a booted instance can see, which is a design decision, not a compiler's.
The fact
composeStacks([...], { manifest: 'preserve' })is deliberately additive: it produces the flattened stack the platform has always produced pluspackages[], whose entries are the same metadata assembled per package. So a compiled multi-package artifact carries every object, view, flow and permission set twice.Measured on
examples/app-multi-package(two packages, one object each):dist/objectstack.jsonis 7.2 KB, of which thepackages[]half is a near-copy of the top-level half. The ratio does not improve with size — it is one extra copy of everything a package owns.Why both copies exist today
They feed two different readers, and each reader ignores the other's copy:
MetadataPlugin._parseAndRegisterArtifactiterates (ARTIFACT_FIELD_TO_TYPEover the parsed definition,packages/metadata/src/plugin.ts). It never looks atpackages. Drop the top level and a booted instance has no views, flows, permission sets or datasets in the metadata service at all.packages[]is whatObjectQL.registerAppiterates, one manifest at a time, and it is the only thing that can stamp per-package ownership (_packageId, the namespace-owner set). Drop it and the artifact installs N package records owning nothing — which is the state ADR-0130 D4 existed to leave behind.D4's read-both rule is what makes the second point sharp: when
packagesis present the load path registers through it, so the top-level collections reachregisterAppnot at all.Why it matters
ADR-0130 D4 reserves the
{ ref, integrity }external-segment position specifically because artifact size is a real constraint — 黑猫's single-package artifact is already 2.6 MB, and both marketplace transfer and startup parse scale with it. Doubling the payload is the opposite direction, and it arrives at the same moment the format gains the key that was meant to help.There is a correctness edge too, though nothing exercises it today: two copies of one definition can be edited apart. Nothing reconciles them, and which one a consumer sees depends on which reader it went through.
What the decision is
packages[]— iterate the package bodies when the key is present, exactly as the ObjectQL side does — and stop emitting the flattened collections for multi-package artifacts. Single-package artifacts keep today's shape untouched (D4/D7). This is the one that removes the copy rather than compressing it.packages[i]referencing top-level items by name. Cheapest on bytes, and the one that most deserves scrutiny: a reference that resolves at load is a second lookup path for metadata identity, and a dangling one fails silently.Not urgent: no customer artifact carries
packages[]yet, so this can be decided before it is expensive rather than after.Triage — graded
Located at
origin/main4a37870:composeStacksispackages/spec/src/stack.zod.ts:2451; the metadata-side reader ispackages/metadata/src/plugin.ts:72(ARTIFACT_FIELD_TO_TYPE) consumed at:935. The artifact shape is apackages/speccontract, which is what sets the lane.The card needs no correction — it states its own three options fairly, names the correctness edge without overclaiming it ("nothing exercises it today"), and is explicit that no customer artifact carries the key yet. Graded as a decision because every option changes the compiled artifact format, which is a published contract with an ADR (ADR-0130 D4/D7) on it.
<!-- os-decision-facets -->
packages[]。packages[]目前没有任何客户产物,这是唯一一次可以零成本收敛的窗口,过了就得带着两份副本做兼容。推荐:A = 卡面的方案 B。 教元数据产物读取方在
packages[]存在时按包体遍历,多包产物不再输出扁平集合;单包产物形状原样不动(D4/D7)。①领起 —— 它删掉一份,而不是新增一条查找路径;③让改岔在结构上不可能;④不新增格式概念;②的零拉动不反对它,反而正是它现在做最便宜的理由。回退:B' = 卡面的方案 A(先不动)。 若维护者认为 D4 的外部分段位置本就是为体积准备的、届时一并处理更省事,走这条 —— 代价写明:届时已有客户产物,同一个变更从「零迁移」变成「破坏性」。
⛔ 不荐方案 C。 按名字引用会新增第二条元数据身份解析路径,悬空时静默失败,③④都反对;卡面把它列为「最该被审视的一个」,本席同意并且把它排在最后。
置信缺口(本分析看不见什么): 没有量方案 B 的真实成本。卡面说
MetadataPlugin._parseAndRegisterArtifact今天完全不看packages键,但没有说它要读的那些集合(views / flows / permission sets / datasets)在包体里的形状是否与顶层一致。若不一致,B 就不是「换个地方遍历」而是一次形状适配,成本和推荐都要重排。这是执行 B 之前必须先量的第一件事,不是执行中顺手确认的事。Generated by Claude Code