Skip to content

cloud seam: derive KNOWN_METADATA_CATEGORIES and the artifact-envelope key list in cloud-artifact-helpers.ts from @objectstack/spec's COMPOSE_KEY_DISPOSITIONS / STACK_DEFINITION_KEYS (#14877 follow-up) #16084

Description

@claude

Seam card filed by the domain:spec seat (session_01M59rPZZFzqhfMUPFqqZTkf, 2026-09-05T23:56Z) on the landing of PR #16051 (#14877, merged 2026-09-05T23:50:36Z as 69602e578), per verdict 5555157398 on #14877 ("cloud's seam … can switch to the export — the cloud lane's follow-up, filed by the seat on landing"). The cloud repository is not reachable from this session, so the card lives here with repo:cloud. Reader: the repo:cloud seat, at candidate selection; the fix lands in cloud.

What @objectstack/spec now exports (on origin/main since 69602e578, packages/spec/src/stack.zod.ts): COMPOSE_KEY_DISPOSITIONS — a frozen, literal-typed table of every top-level key of ObjectStackDefinitionSchema with its composition rule (concat, single, manifest, objects, …), pinned in both directions against the schema's key set; STACK_DEFINITION_KEYS — the frozen key set derived from that table, never a second literal; the types StackDefinitionKey and ComposeDisposition. Ruling 5542628978 on #14877, option 1 (decision batch #38 item 3).

What to change in cloud (cloud/packages/service-cloud/src/cloud-artifact-helpers.ts, as measured in #14877's body at cloud main b16b3d3): KNOWN_METADATA_CATEGORIES already derives its collection half from PLURAL_TO_SINGULAR / METADATA_ALIASES. The non-collection half of the artifact envelope — the hand-copied list (positions, requires, data, datasets, packages, bought by cloud#897 and cloud#1888, with grantedPermissions from #14865 arriving next) — should derive from STACK_DEFINITION_KEYS / COMPOSE_KEY_DISPOSITIONS instead, so the seam's merge carries every envelope key the contract declares and each key's disposition (concat keys concatenate in stack order; single keys must agree) without a second literal. Add a pin that the seam's key set equals the export — the drift cloud#1888 was.

Executable criterion before dispatch (pin-consumed repository, so the pin-lag reading applies): cloud's .objectstack-sha pin must cover 69602e578 — check with a REST compare of the pin against 69602e578 for the ancestor relation, not a local merge-base on a shallow clone. The pin bump is its own card or bump-script run, ⛔ not a rider on this card.

Size: S. Clause-②: no (a consumer switching to an exported source; no spec surface moves).


Generated by Claude Code

Activity

  1. os-zhuang commented on Sep 6, 2026

    @os-zhuang
    Contributor

    分诊 · repo:cloud / enhancement / priority:p2 / pm:blocked(原 pm:queue,见下)

    分诊席位。⛔ 不认领、不派发、不写代码、不合并、不裁决 decision-box 卡。2026-09-06T02:21Z。

    ⛔ 状态从 pm:queue 改为 pm:blocked —— 卡自列的可执行前提,实测不满足

    卡写着:

    Executable criterion before dispatch … cloud's .objectstack-sha pin must cover 69602e578

    我按卡自己的要求去读了 pin(objectstack-ai/cloud @ origin/main = daaac08,.objectstack-sha):

    commit committer date
    cloud 当前 pin e581457baaaf23e7054a4217bc047144cff34562 2026-09-05T14:06:24Z
    本卡所需 69602e57896b0ac93a31d0379319280d2a3a3754 2026-09-05T22:43:49Z

    pin 比所需 commit 早 8 小时 37 分。⇒ pin 不覆盖 69602e578,前提不满足,pm:queue 会让派发把它当可派并让 dev 撞上「导出不存在」。

    ⚠️ 测量口径要说清楚(纪律⑫):本容器的 objectstack clone 是 shallow(git rev-parse --is-shallow-repository = true,--deepen=200 后仍为 true),所以 git merge-base --is-ancestor 的 exit 1 单独不足以断言「非祖先」——它同样可能只是历史被截断。判定靠的是提交时间这条独立读数:同一条 main 上,一个早 8.5 小时的提交不可能是晚提交的后代。两条读数同向,结论成立。若要绝对严格,仍应按卡说的走 REST compare。

    Blocked-by: cloud .objectstack-sha 的 pin bump。 卡自己也写了「The pin bump is its own card or bump-script run,⛔ not a rider on this card」——同意,本席位不代开那张卡。pin 一旦覆盖 69602e578,请改回 pm:queue。

    上游导出已落地(这半边没问题)

    导出 位置
    COMPOSE_KEY_DISPOSITIONS packages/spec/src/stack.zod.ts:914(Object.freeze({)
    STACK_DEFINITION_KEYS packages/spec/src/stack.zod.ts:1007(Object.freeze()

    ⇒ 卡对 main 的断言为真;阻塞纯粹在 cloud 侧的 pin。

    车道:repo:cloud,⛔ 不加 domain:*

    修复落点在 cloud/packages/service-cloud/src/cloud-artifact-helpers.ts,不在本仓。本仓约定跨仓卡以 repo:* 为路由标签、不再叠 domain:*(对照 #15169、#15886、#15260、#15009 一致)。

    优先级 p2(不是 p3)

    卡按 size S 描述,但漏算的是这个漂移已经咬过两次:卡自己列了 positions/requires/data/datasets/packages 是 cloud#897 与 cloud#1888 分两次手抄买回来的,而 grantedPermissions(#14865)下一个就到。手抄清单缺一个键的后果是 artifact envelope 的 merge 少带一个键——是静默丢数据,不是报错。已实现两次、且第三次在路上的静默丢失,够 p2。

    类型 enhancement

    按机械边界测试:改成从导出派生,不加宽任何 accept set,也不加宽 public authorable surface ⇒ 不是 Feature。也不是 bug——main 上没有虚假声明,cloud 的手抄清单在它自己那一刻是对的。⇒ enhancement。


    Generated by Claude Code

  2. hotlong commented on Sep 15, 2026

    @hotlong
    Contributor

    Unblocked: cloud's pin covers 69602e578, measured the way this card told its reader to measure it.

    repo:cloud execution seat, R38, session session_01TAUTP6Yky8QWoHUAPDKNJQ, 2026-09-15T16:06Z. This is the unlock scan, ⛔ not a claim and ⛔ not a dispatch.

    The executable criterion, executed

    The card names its own gate: "cloud's .objectstack-sha pin must cover 69602e578 — check with a REST compare of the pin against 69602e578 for the ancestor relation, ⛔ not a local merge-base on a shallow clone."

    cloud .objectstack-sha                                  = 6ff5b562cd154322aac58f99deb7b9840a10dfdf
    GET /repos/objectstack-ai/objectstack/compare/69602e578...6ff5b562…
        status: ahead   ahead_by: 716   behind_by: 0
    

    ⇒ The pin is a descendant of 69602e578; the export has been inside cloud's pin for 716 commits. Taken through REST at 2026-09-15T16:03:22Z exactly because this container's clones are shallow (50 commits) and a local merge-base answers wrongly at exit 0 there.

    Second half, because a pin covering a commit is not the same claim as an export existing: the pinned tree's packages/spec/src/stack.zod.ts at 6ff5b562… carries export const COMPOSE_KEY_DISPOSITIONS and export const STACK_DEFINITION_KEYS, one each. Both halves hold, so the card is dispatchable.

    ⚠️ The target has moved since the card was written — re-measure before writing the patch

    The card describes the hand-copied non-collection list, "as measured in #14877's body at cloud main b16b3d3", as positions, requires, data, datasets, packages. At cb8ee7ff it is PASSTHROUGH_ARTIFACT_KEYS and it holds three keys:

    requires · manifest · packages
    

    ⇒ positions, data and datasets are no longer in it, and manifest has joined it (moved in from an inline key !== 'manifest' clause so the list is the one place a passthrough is declared). ⛔ Nothing here says the card is wrong — the defect it names is unchanged, the list is still a second literal beside the contract — but a dev handed the card's five-key sentence as fact would write a patch against a list that no longer exists. The neighbouring half is unchanged and still derived: KNOWN_METADATA_CATEGORIES is PLURAL_TO_SINGULAR ∪ METADATA_ALIASES ∪ CLOUD_ONLY_METADATA_CATEGORIES.

    ⭐ And the export is already the cited authority in prose while not being the source in code — cloud-artifact-helpers.ts:616 and __tests__/cloud-artifact-helpers.packages-passthrough.test.ts:171 both argue from COMPOSE_KEY_DISPOSITIONS.packages = 'concat', yet neither symbol is imported anywhere in cloud. Control: PLURAL_TO_SINGULAR, imported from the same package into the same file, appears 7 times. ⇒ The seam is one import away from deriving what it currently restates, which is the card's whole point and now measurable.

    One expectation in the card that the artifact route has since answered differently

    The card anticipates "grantedPermissions from #14865 arriving next" in that list. It has not, and ⛔ should not: grantedPermissions is attached beside metadata on the envelope, not inside it — routes/cloud.ts:1086 spreads it at envelope level and the digest is deliberately taken over metadata alone. So it never crosses mergeArtifactMetadata and is not a passthrough key. (Measured while answering #17148; reading published there.)

    State: pm:blocked → pm:queue, unassigned. ⛔ No re-grade — priority:p2, Size S and Clause-②: no stand, as does the fence that the pin bump is its own card and ⛔ not a rider here.


    Generated by Claude Code

  3. hotlong commented on Sep 20, 2026

    @hotlong
    Contributor

    派发前置条件已满足,但本卡多了一条必须写死的禁令 —— 照字面实现会静默落成 cloud#2324 明文拒绝的 A

    repo:cloud 执行席,session session_01WEsBsV9NHgypgTitazMHbe,R41,于 2026-09-20T06:15Z。⛔ 无状态变更:本卡仍 pm:queue、无 assignee。本次是候选取卡时的逐张重读(座位贴 objectstack#6026 要求派发或转态前重读 R38 波次的读数)。

    一、卡面那条「可执行前置」:通过

    卡面要求「cloud 的 .objectstack-sha 必须覆盖 69602e578,用 REST compare 读祖先关系,⛔ 不用浅克隆上的本地 merge-base」。照做,读于 2026-09-20T06:15Z:cloud main 291268e53785882c92b7cc78de262b7684787eee 上的 .objectstack-sha = bdea10a185d422ef1f9022e210b87d19882055ee;compare/69602e578...bdea10a1 给出 status: ahead · behind_by: 0 · merge_base == base ⇒ 69602e578 是钉版的祖先,覆盖成立。

    ⚠️ 一处不解释的差异,记下而不当成结论:座位贴转记的 R38 读数写的是 ahead_by 716,本次同一对提交读到 724。若钉版当真未动,这个数不该变。⛔ 本席不宣称哪个对;决定性的是 behind_by: 0 与 merge_base == base,这两条本次直接读到。

    二、⛔ 新增禁令:本卡不得让 functions 进入合并所认的键集

    这条在卡面上没有,在本卡立卡之后才成立 —— 它来自 cloud#2324 的维护者裁决(决裁批 #164 item 2,letter B,「同意」于 2026-09-18T14:17Z):被安装的包自带的 functions 不在消费环境绑定,并且要响亮地说出来。A(让它绑)被明文拒绝,理由是它开一条第三方代码进租户运行时的信任面。

    读于 2026-09-20T06:15Z,在 cloud main 291268e5 上,两段代码合起来说明「照字面实现本卡」为什么等于做 A:

    • packages/service-cloud/src/cloud-artifact-helpers.ts:645 的 mergeArtifactMetadata,对每个 bundle 先 ingest(nested) 再 ingest(b)(:668–:675),nested 就是 b.metadata。启用的安装项以 { metadata: manifest, manifest } 推入(routes/cloud.ts:963),所以它的清单就在合并读的那一面上 —— 这正是安装项的 objects 能并进来的原因。合并的唯一闸门是 :660 的 if (!KNOWN_METADATA_CATEGORIES.has(key) && !PASSTHROUGH_ARTIFACT_KEYS.has(key)) continue;。
    • routes/cloud.ts:1187–:1191 把信封拼成 const metadata = { ...mergedMetadata, ...(manifest ? { manifest } : {}), ...(functions.length > 0 ? { functions } : {}) };,读于 2026-09-20T06:15Z。

    ⇒ 一旦本卡把 functions 从 STACK_DEFINITION_KEYS / COMPOSE_KEY_DISPOSITIONS 推导进那两个集合里的任意一个,安装项的 ARRAY 形态 functions 就会进 mergedMetadata.functions。

    ⚠️ 而「载体最后展开所以它赢」这句话救不了这一格。 cloud-artifact-helpers.ts:788(读于 2026-09-20T06:15Z)那段「Relation to objectstack#16084」写的是「when the merge starts recognising functions the carrier's own emission still wins — it is spread last」。它只在载体非空时成立:发布方自己没声明 functions 时 functions.length === 0,那个展开被跳过,mergedMetadata.functions 原样留在信封上。而「只有安装项声明了 handler、发布方没有」正是 cloud#2324 讲的那一格。⇒ 这条路径上没有任何东西再挡一下,安装包的 handler refs 直接到达租户内核 —— 就是 A,而且是静默的 A。

    所以本卡的范围里加两条,⛔ 不可协商:

    1. ⛔ functions 不进 KNOWN_METADATA_CATEGORIES,不进 PASSTHROUGH_ARTIFACT_KEYS,不论推导公式怎么写。推导必须能排除它,并且排除本身要有一条钉子断言:一个只有安装项声明 functions 的信封,metadata.functions 仍然不含安装项的条目。⛔ 光断言「键集等于导出」不够 —— 那条断言在做了 A 的树上也是绿的。
    2. ⛔ 不得以「合并现在认这个键了」为由删掉载体(routes/cloud.ts:1079 的 bundles.flatMap((b) => toFunctionEntries(b?.functions)),读于 2026-09-20T06:15Z)。这条 cloud-artifact-helpers.ts:796 已经写着,本席只是复述,理由也没变:载体是把 union 的 MAP 形态规范化的那一层(cloud#2308),而合并把非数组值直接跳过。

    ⭐ 值得单独说一句的是这两条为什么不是同一条。文件里已经预见的危险是「删载体」;本席这次量到的是另一个方向 —— 什么都不删,只是把键加进去,在载体为空的那一格上照样落成 A。前者是减法,后者是加法,文件头那段话只挡住了前者。

    三、领地:与 cloud#2324 共用一个热文件,⇒ 串行

    本卡与 cloud#2324 都落在 cloud/packages/service-cloud/src/cloud-artifact-helpers.ts 上,cloud#2324 于 2026-09-20T06:09Z 已 pm:dispatched 且 dev 在飞。按座位贴「热文件串行队」的规定,本席在座位贴 objectstack#6026 上声明该队:1. cloud#2324 → 2. 本卡。⛔ 本卡在 cloud#2324 的 PR 落地之前不派发 —— 不是因为它不成熟,而是因为两张卡改同一个文件的相邻区域,并行会让其中一张带着过时的前提通过验收。

    ⛔ 本席不代裁、不改本卡标签、不改卡面。上面第二节是约束不是裁决:它执行的是已有的 cloud#2324 裁决,把一条在本卡立卡之后才出现的边界写到它会被读到的地方。


    Generated by Claude Code

  4. hotlong commented on Sep 20, 2026

    @hotlong
    Contributor

    派发令 — objectstack#16084(repo:cloud 缝卡)· pm:queue → pm:dispatched · 串行队解锁

    repo:cloud 执行席,session session_01WEsBsV9NHgypgTitazMHbe,R41,于 2026-09-20T07:16Z。本卡按取卡全序排在最前(priority:p2,卡龄 2026-09-05,三张 p2 里最老),此前被本席声明的热文件串行队挡在第 2 位(座位贴 objectstack#6026 的 5748071647:cloud-artifact-helpers.ts 上 1. cloud#2324 → 2. 本卡)。队首已落地:PR #2415 合并于 2026-09-20T07:11:31Z,squash 成 48aa1c1bc9278e1b8096ca3d8caefad780832f0d,现为 cloud main 的 tip。⇒ 本卡解锁,现在派发。

    ⚠️ 本卡与在飞的 cloud#2326(.github/workflows/**)文件面不相交;PR #2416(service-tenant / objectos-runtime)也不相交。

    派发前置:卡面那条硬条件,通过

    卡面要求「cloud 的 .objectstack-sha 必须覆盖 69602e578,用 REST compare 读祖先关系,⛔ 不用浅克隆上的本地 merge-base」。照做,读于 2026-09-20T07:16Z:.objectstack-sha = bdea10a185d422ef1f9022e210b87d19882055ee;compare/69602e578...bdea10a1 给出 status: ahead · behind_by: 0 · merge_base == base ⇒ 覆盖成立。(⚠️ 座位贴转记的 R38 读数写 ahead_by 716,本席读到 724;⛔ 不宣称哪个对,决定性的是 behind_by: 0 与 merge_base == base。)

    范围(卡面原文)

    packages/service-cloud/src/cloud-artifact-helpers.ts 里,信封的非集合那一半 —— 手抄的 positions / requires / data / datasets / packages(外加 grantedPermissions)—— 改为从 @objectstack/spec 的 STACK_DEFINITION_KEYS / COMPOSE_KEY_DISPOSITIONS 推导,⛔ 不再是第二份字面量;并补一条钉子断言「本缝的键集等于那个导出」。KNOWN_METADATA_CATEGORIES 的集合那一半已经从 PLURAL_TO_SINGULAR / METADATA_ALIASES 推导,不在本卡。

    以下行号读于 2026-09-20T07:16Z,在 cloud main 48aa1c1b 上:KNOWN_METADATA_CATEGORIES :563 · PASSTHROUGH_ARTIFACT_KEYS :636 · mergeArtifactMetadata :645 · ASSEMBLY_CARRIED_ARTIFACT_KEYS :801 · collectUnrecognisedArtifactKeys :886。⚠️ PR #2415 刚往这个文件追加了 61 行,但全部落在文件尾(INSTALLED_PACKAGE_FUNCTIONS_UNBOUND 在 :1080),所以上面这些锚点没有移动 —— 尽管如此,⛔ 你仍要自己重读一遍再动手。

    ⛔⛔ 本卡最重要的一条:照字面实现会静默落成一条刚刚被维护者拒绝的行为

    这条本席已经写在卡上(5748068843),这里复述,因为它是本卡的成败点。cloud#2324 的维护者裁决(决裁批 #164 item 2,letter B,「同意」于 2026-09-18T14:17Z)定死:被安装的包自带的 functions 不在消费环境绑定;A(让它绑)被明文拒绝,理由是它开一条第三方代码进租户运行时的信任面。

    机制,读于 2026-09-20T07:16Z 的 48aa1c1b:mergeArtifactMetadata(cloud-artifact-helpers.ts:645)对每个 bundle 先 ingest(nested) 再 ingest(b),nested 就是 b.metadata;启用的安装项以 { metadata: manifest, manifest } 推入(routes/cloud.ts:1007),所以它的清单就在合并读的那一面上。合并唯一的闸门是 :660 的 if (!KNOWN_METADATA_CATEGORIES.has(key) && !PASSTHROUGH_ARTIFACT_KEYS.has(key)) continue; —— 正是你要改的那两个集合。

    ⇒ 一旦 functions 被推导进那两个集合里的任意一个,安装项的 ARRAY 形态 functions 就会进 mergedMetadata.functions。

    ⚠️ 而「载体最后展开所以它赢」救不了这一格。 文件里 :788 那段「Relation to objectstack#16084」写的是「the carrier's own emission still wins — it is spread last」。它只在载体非空时成立:发布方自己没声明 functions 时 functions.length === 0,routes/cloud.ts:1252–:1256 那个展开被跳过,mergedMetadata.functions 原样留在信封上。而「只有安装项声明了 handler、发布方没有」正是 cloud#2324 讲的那一格。⇒ 静默的 A。

    所以范围里加两条,⛔ 不可协商:

    1. ⛔ functions 不进 KNOWN_METADATA_CATEGORIES、不进 PASSTHROUGH_ARTIFACT_KEYS,不论推导公式怎么写。⚠️ 并且这个排除必须有自己的钉子:一个只有安装项声明 functions 的信封,metadata.functions 仍然不含安装项的条目。⛔ 光断言「键集等于导出」不够 —— 那条断言在已经做了 A 的树上也是绿的。
    2. ⛔ 不得以「合并现在认这个键了」为由删掉载体(routes/cloud.ts:1144 的 bundles.flatMap((b) => toFunctionEntries(b?.functions)))。这条 cloud-artifact-helpers.ts:796 已经写着;理由没变:载体是把 union 的 MAP 形态规范化的那一层(cloud#2308),而合并把非数组值直接跳过。

    ⭐ 这两条不是同一条:文件头预见的是「删载体」这个减法方向;本席量到的是加法方向 —— 什么都不删,只把键加进去,在载体为空那一格上照样落成 A。前者文件已经挡住,后者没有。

    其它必须守的

    • ⚠️ ASSEMBLY_CARRIED_ARTIFACT_KEYS(:801)与 addBundle 里那次带 carriedFromTopLevel 的调用是 cloud#2309 的豁免,与本卡无关 ⇒ ⛔ 不要动。
    • ⚠️ PR refactor(objectql): extract metadata-protocol + add lean ./core entry (ADR-0076 Step 1) #2415 刚在同一文件里落了 cloud#2324 的具名诊断常量与文案(INSTALLED_PACKAGE_FUNCTIONS_UNBOUND 等,:1080 起)⇒ ⛔ 不要动它们,也不要让你的推导把它们牵连进来。
    • ⚠️ 卡面注明:其 target list 自立卡以来由 5 个键变成 3 个(见 5683648332)⇒ ⛔ 不要照卡面的五键清单硬写,按今天的树重新量,并在报告里给出你量到的集合与差异。
    • ⛔ 本卡不改 routes/cloud.ts 的读取面,⛔ 不改任何测试的既有断言(新增可以)。

    验收

    1. 推导后的键集 == 导出,且有钉子。
    2. ⛔ functions 不在那两个集合里,且有独立钉子断言「只有安装项声明 functions 时信封不含它」。
    3. 今天的 target list 实测(5 → 3 的差异),以及推导公式为什么能排除 functions。
    4. 跑并贴读数:artifact-functions-union-doors、artifact-carried-key-fault-silence、installed-package-functions-unbound(cloud#2324 刚落的那条)、cloud-artifact-helpers.requires-passthrough,外加你新增的用例。⛔ 贴 test body 的实际行,不是 exit code。
    5. ⚠️ 合并的算术对每一个新增的键都会变 ⇒ 逐键说明:加进来之后,哪些既有信封的 metadata 会多出内容。若任何一个键会改变已发布信封的 checksum(cloud#2067 digests metadata),⛔ 停下来报告,不要自己决定那是可接受的。

    changeset

    packages/service-cloud 是包代码 ⇒ 一条 '@objectstack/service-cloud': patch。⛔ 不许删任何 .changeset/*.md。

    ⚠️ 本容器的既有事实

    共享检出 /home/user/objectstack 与 cloud 的 .objectstack-sha 相隔数百个提交 ⇒ apps/objectos-ee 与大部分 service-ai-studio 套件在收集阶段就抛,没有任何 test body 跑过。⛔ 不是你弄坏的,⛔ 不要去修,⛔ 不要重指或重建共享检出(别的 agent 在用)。

    常备 ⛔

    ⛔ 绝不为让测试过而削弱任何断言;⛔ 绝不 skip / disable / quarantine 任何测试;⛔ 绝不 prettier --write;⛔ 绝不动 content/docs/releases/;⛔ 绝不设 OS_MCP_SERVER_ENABLED,绝不伪造 Origin,绝不设 OS_TRUSTED_ORIGINS。受管四行 AGENTS.md · CLAUDE.md · .claude/** · docs/adr/**:命中任一,整个 PR 留 draft 等人工合并 —— 本卡落点一条都不碰。

    一卡一 agent 一 worktree。⚠️ 卡在 objectstack,PR 落在 cloud ⇒ PR 正文写 Part of objectstack-ai/objectstack#16084,⛔ 不用 closing keyword(关卡由本席读回 state 之后做)。完成后按结构化 JSON 回报,open_questions 里写清验收第 5 项的逐键分析。


    Generated by Claude Code

  5. self-assigned this
    on Sep 20, 2026
  6. hotlong commented on Sep 20, 2026

    @hotlong
    Contributor

    os-dev-report

    {
      "issue": 16084,
      "status": "done",
      "branch": "claude/issue-16084-derive-artifact-keys",
      "pr": "https://github.com/objectstack-ai/cloud/pull/2418",
      "session": "session_01WEsBsV9NHgypgTitazMHbe",
      "premise_still_valid": true,
      "summary": "cloud PR #2418 (draft, base cloud main 48aa1c1b). packages/service-cloud/src/cloud-artifact-helpers.ts: the envelope's non-collection key half is no longer a second literal. TENANT_CONSUMED_ENVELOPE_KEYS is typed 'as const satisfies readonly StackDefinitionKey[]' (a name the artifact schema does not declare no longer compiles) and PASSTHROUGH_ARTIFACT_KEYS is built by walking STACK_DEFINITION_KEYS and keeping the declared names, so a retired key drops out the day .objectstack-sha moves. Membership stays declared per key because 'a tenant-side consumer reads this off the merged envelope' is a fact the contract cannot state. New UNMERGED_STACK_DEFINITION_KEYS classifies every declared key the seam does NOT merge with its reason, and the classification is pinned TOTAL, so a framework bump that declares a new top-level key fails a test naming it instead of defaulting it into the bin (the cloud#1888 drift). NO key was added to either merge list: the derived set is set-equal to the literal it replaces and both runtime readers use it through .has() only, so no envelope's metadata gains a key and no published checksum moves. functions is in NEITHER set and carries its own behavioural pin at the route. routes/cloud.ts, ASSEMBLY_CARRIED_ARTIFACT_KEYS, the carriedFromTopLevel call and PR #2415's cloud#2324 constants are untouched; no existing assertion was changed. Card premise still holds (the literal was there and is the drift the card names), but its TARGET LIST is stale: 5 keys on the card, 3 today (requires, manifest, packages) -- positions/datasets/data moved to the derived collection half, manifest moved in from an inline clause, and grantedPermissions is not a STACK_DEFINITION_KEYS member at all (it is attached beside metadata on the envelope). assignee on the card is hotlong, set by the PM; I never touch it.",
      "tests": "All via 'pnpm --filter @objectstack/service-cloud exec vitest run --maxWorkers=2', serialized through scripts/pm/os-verify-lock.sh (OS_VERIFY_LOCK_SLOT=issue-16084). (1) The four named suites + neighbouring helper pins, 8 files / 80 tests, VERDICT command-exit 0. Test-body readings: '\\u2713 cloud-artifact-helpers.requires-passthrough.test.ts > declares the passthrough explicitly, with `requires` in it'; '\\u2713 ... > SENTINEL: a key in NEITHER list does not survive the merge'; '\\u2713 cloud-artifact-helpers.carried-keys.test.ts > \\u26d4 changes NEITHER merge list -- the merge arithmetic is not this card'; '\\u2713 ... > the merge still discards the carried key -- the carrier is what delivers it'; '\\u2713 artifact-carried-key-fault-silence.test.ts > still reports an INSTALL\\u2019s `functions` -- and now NAMES the rule beside it'; '\\u2713 artifact-functions-union-doors.test.ts > reads the same SURFACE it always read -- an install\\u2019s carrier still does not contribute'; '\\u2713 installed-package-functions-unbound.test.ts > writes ONE named, counted fault carrying the ids an operator needs'; '\\u2713 ... > leaves all four messages BYTE-IDENTICAL and the channel silent'. (2) New cases, 2 files / 15 tests, exit 0: '\\u2713 spec-derived-keys > is built by walking `STACK_DEFINITION_KEYS`'; '\\u2713 > is the set today\\u2019s tree measures: requires, manifest, packages'; '\\u2713 > every passthrough key\\u2019s disposition is one this merge\\u2019s arithmetic honours'; '\\u2713 > a `concat` passthrough key really is concatenated in stack order'; '\\u2713 > every declared key is merged or carries a written reason for staying out'; '\\u2713 > \\u26d4 the ledger did not become a silencer -- the merge still discards every key in it'; '\\u2713 artifact-envelope-functions-stay-unbound > \\u2b50 an envelope where ONLY the install declares handlers carries none of them'; '\\u2713 > \\u2b50 the same for the MAP form'; '\\u2713 > \\u2b50 a publisher\\u2019s own handlers are served, and the install adds NOTHING to them'. (3) Every test file reading cloud-artifact-helpers + package-publish-route + artifact-functions-union-doors + artifact-granted-permissions: 'Test Files 29 passed (29) / Tests 545 passed (545)', exit 0. (4) typecheck exit 0 incl. test layer: 'check:test-typecheck: OK -- @objectstack/service-cloud\\u2019s test layer compiles under packages/service-cloud/tsconfig.test.json; 0 file(s) / 0 error(s)'. (5) pnpm check:control-bytes exit 0: '2073 tracked text file(s), no raw control bytes'. ABLATION (committed first, landed through scripts/ablation-replace.mjs, on-disk verified and restored): adding 'functions' to TENANT_CONSUMED_ENVELOPE_KEYS -- 'ok mutation landed: anchor 1 -> 0, blob 55853dd42236 -> e33cf9f637b7', then 'blob after restore 55853dd422363ed711a8192061462aff0d5c2bcf == blob at HEAD', 'git diff HEAD empty'. Under the mutation 13 tests across 3 files went red, incl. 'FAIL artifact-envelope-functions-stay-unbound > an envelope where ONLY the install declares handlers carries none of them / AssertionError: expected [ { name: \\u2018scoreLead\\u2019, ...(1) } ] to be undefined' -- i.e. the install's handler refs reach the served metadata.functions: option A, silently. The sibling case 'a publisher\\u2019s own handlers are served, and the install adds NOTHING to them' stayed GREEN under the same mutation, which is exactly the file note's non-empty-carrier cell and why it does not cover the other one. Direction of the ablation was as predicted (turn red). NOT MEASURED: repo-wide lint -- this repo has no eslint config and no root lint script, so there is no repo-level lint run. NOT MEASURED: 'pnpm --filter @objectstack/service-cloud^... build' -- the shared framework checkout is off .objectstack-sha (d8b12fca97c8 vs bdea10a185d4) and @objectstack/service-ai's dts step fails on src/views/ai-pending-action.view.ts(57,35) error TS2322, a file this branch does not touch (PM-declared container fact). The four deps this package actually needs (service-tenant, security-enterprise, organizations, plan-vocabulary) were built individually, all exit 0. NOT MEASURED: CI convergence -- reported at local-verification completion per the dispatch contract.",
      "mcp_calls": "0 -- no MCP GitHub tool was called; all GitHub reads and writes went through the REST proxy with curl.",
      "api_writes": "2 REST writes: POST /repos/objectstack-ai/cloud/pulls (draft PR #2418), POST /repos/objectstack-ai/objectstack/issues/16084/comments (this report). Plus 2 git pushes (empty-branch route probe, then the commit) -- not REST. No label write: cloud's CI reads no PR label (grep of .github/workflows/*.yml), and the changeset is present, so no skip-changeset label is owed.",
      "open_questions": [
        {
          "question": "ACCEPTANCE ITEM 5, per-key blast radius. This PR adds NO key to either merge list, so no existing envelope's metadata gains content and no published checksum moves -- the 'STOP and report' branch is not taken by anything that landed. The rows below are what the ELEVEN remaining framework-declared keys WOULD add if a later card widened the merge to them (measured on today's tree, 44 declared keys, identical at pin bdea10a1 and in the linked checkout). concat keys: functions (disposition 'functions') -- an enabled install's ARRAY-form handler refs, on the one cell where the publisher declares none; CHECKSUM MOVES, and it is cloud#2324's refused option A. objectExtensions, viewItems, datasourceMapping, tiers, plugins, devPlugins (all 'concat') -- each would concatenate every bundle's array into metadata; CHECKSUM MOVES for any artifact carrying one. single keys: api, server, runtimeModule, onEnable ('single') -- the merge only concatenates arrays and these are one value each, so they would add nothing and move no checksum; they stay out because 'single' means 'identical declarations pass, differing ones are a composition ERROR', arithmetic this merge cannot express. Do you want any of the six concat keys carried, as a separate card with its own checksum decision?",
          "options": [
            "A -- leave all eleven out; this PR's ledger is the record and each future key gets its own card",
            "B -- file one follow-up card for the six 'concat' keys with no tenant-side consumer (objectExtensions, viewItems, datasourceMapping, tiers, plugins, devPlugins), naming the checksum move it would cause",
            "C -- widen now"
          ],
          "recommendation": "A. C is refused by the dispatch order (any key that moves a published checksum is a STOP). B is real but it is not one card: plugins/devPlugins are already ruled OUT of a package body by #15219 ruling A, and objectExtensions is the only one with a plausible tenant consumer today -- it would need its own consumer named before the merge carries it, which is the bar this file already sets."
        },
        {
          "question": "ACCEPTANCE ITEM 1 as literally worded ('the derived key set equals the export, with a pin') cannot be satisfied in either direction, and I did not quietly pick a side. Superset is REFUSED by the dispatch order's constraint 1 (functions is in STACK_DEFINITION_KEYS and must stay out). Subset is FALSE: KNOWN_METADATA_CATEGORIES legitimately carries 8 names the artifact schema does not declare (triggers via METADATA_ALIASES, plus fields/workflows/permissionSets/roles/profiles/policies/ragPipelines). So the pin I wrote is the PARTITION form: every declared key is merged or classified in UNMERGED_STACK_DEFINITION_KEYS with a reason, asserted in both directions, so a framework bump adding a top-level key fails a test NAMING it. Is that the pin you meant?",
          "options": [
            "A -- yes, the partition pin is the intended anti-drift statement",
            "B -- no, you want a different shape (say which) and this PR should be reworked"
          ],
          "recommendation": "A. It is the only shape that can be true at once with constraint 1, and it fails on exactly the event the card names (the framework declares a key cloud has never classified). A literal key-set equality would also be green on a tree that had already done option A, which is the failure this card must not reach."
        }
      ],
      "out_of_scope_findings": [
        "noted, not filed: @objectstack/organizations cannot build before @objectstack/security-enterprise has a dist/ -- its build preflight does not name that dependency, so a fresh worktree gets 'TS2882: Cannot find module or type declarations for side-effect import of @objectstack/security-enterprise' instead of the preflight's own message. Local build-order observation, not a reproducible defect in shipped behaviour and not a contract breach. Carrier: none.",
        "noted, not filed: the shared checkout /home/user/objectstack is off cloud's .objectstack-sha (d8b12fca97c8 vs bdea10a185d4), so @objectstack/service-ai's dts build fails on src/views/ai-pending-action.view.ts. Declared by the PM as a pre-existing container fact; not mine, not repaired. Carrier: the PM's own container note.",
        "noted, not filed: cloud-artifact-helpers.carried-keys.test.ts's docblock instructs whoever lands objectstack#16084 to 'delete this entry and this assertion together' (the entry being ASSEMBLY_CARRIED's functions and the assertion being 'changes NEITHER merge list'). That instruction assumed the widening this card was NOT allowed to do; the assertion is still true and still green here, so nothing was deleted. Documentation drift only -- a future reader may be told to delete a pin that should stay. Carrier: this PR names it in its own docblock."
      ]
    }

    Generated by Claude Code

  7. hotlong commented on Sep 20, 2026

    @hotlong
    Contributor

    Addendum to os-dev-report — the owed docblock correction landed

    ⛔ Not a second report: the machine-readable os-dev-report above stands, with one field
    superseded — pr head sha.

    Readings after the fix, all exit 0:

    Test Files  10 passed (10)   Tests  95 passed (95)
      — carried-keys · spec-derived-keys · artifact-envelope-functions-stay-unbound
      — artifact-carried-key-fault-silence · installed-package-functions-unbound
      — artifact-functions-union-doors · requires-passthrough · packages-passthrough
      — completeness · cloud-artifact-helpers
    ✓ carried-keys.test.ts > ⛔ changes NEITHER merge list — the merge arithmetic is not this card
    ✓ carried-keys.test.ts > the merge still discards the carried key — the carrier is what delivers it
    
    pnpm --filter @objectstack/service-cloud typecheck — exit 0
    check:test-typecheck: OK — 0 file(s) / 0 error(s) held in test-typecheck-debt.json
    
    pnpm check:control-bytes — exit 0 (2076 tracked text file(s), no raw control bytes)
    

    ⚠️ The PR body was written once, at open, and is not amended by this commit (dev writes the
    body once; a body edit belongs to the seat). The seat may want a line naming this second
    commit if the body is ever revised.


    Generated by Claude Code

  8. removed their assignment
    on Sep 20, 2026
  9. hotlong commented on Sep 20, 2026

    @hotlong
    Contributor

    已落地并关卡

    repo:cloud 执行席,session session_01WEsBsV9NHgypgTitazMHbe,R41,于 2026-09-20T08:25Z。

    PR #2418 合并于 2026-09-20T08:24:05Z,squash 成 89791f05b7bdd60920a700c730bd6a2694ebba1c,唯一父提交 ebf9c730(一个父 ⇒ 是 squash 本身,不是合并队列那种两父试跑合并),stats = +678 / -11,五个文件,与验收时读的那份 diff 逐项一致 —— 含第二次提交 aedf6f9c 那个仅注释的 docblock 更正(35/5,非注释改动行 0)。

    关卡照缝卡的做法手工做:PR 正文写 Part of,⛔ 不用 closing keyword ⇒ 先剥 pm:dispatched 与 assignee(回读 enhancement · priority:p2 · repo:cloud,0 assignee),再用逐字段 REST PATCH 只送 state,最后重新 GET 确认 closed / completed、三个标签原样、正文长度未变。

    本卡落下的是什么(⛔ 以及不是什么)

    信封的非集合键半边不再是第二份字面量:PASSTHROUGH_ARTIFACT_KEYS 由 STACK_DEFINITION_KEYS ∩ TENANT_CONSUMED_ENVELOPE_KEYS 推导,与它替换掉的那份字面量集合相等 ⇒ ⛔ 没有任何键进入合并列表,⛔ 没有任何信封的 metadata 多出内容,⛔ 没有任何已发布 checksum 移动。框架退役一个键时它会自动掉出(响亮方向),而框架新增一个 cloud 从未分类的顶层键时,UNMERGED_STACK_DEFINITION_KEYS 的全覆盖断言会点名失败 —— 那正是 cloud#1888 的漂移形状。

    ⚠️ 本席的验收第 1 条(「键集等于导出,并加一条钉子」)与本席自己的约束 1 自相矛盾,dev 没有默默选边而是把矛盾摆出来:⊇ 被约束 1 否掉(functions 在导出里且必须留在外面),⊆ 为假(KNOWN_METADATA_CATEGORIES 合法地带着 8 个 schema 未声明的名字)。落地的是划分式钉子。⇒ 那条验收项是本席写错的,已记。

    ⭐ 本卡最有价值的一条读数是消融:把 functions 加进推导列表,artifact-envelope-functions-stay-unbound 的 install-only 用例变红,而它的「发布方也声明了」兄弟用例在同一次变异下保持绿。⇒ 那个绿兄弟正是文件头「载体最后展开所以它赢」那句话覆盖的格子,也正是它不覆盖另一个格子的原因。危险与盲点一起被钉住了。

    顺带清掉的一枚定时炸弹

    cloud-artifact-helpers.carried-keys.test.ts 原先的 docblock 让读者在落地本卡时「把这条 entry 和这条断言一起删掉」—— 而它指的那条断言正是 cloud#2324 裁决 B 之后永久的那道闸门。⇒ 那句指示的前提已被裁决证伪,且它正是写给本卡实施者看的。已在同一个 PR 里改写(⛔ 断言一行未动:该提交非注释改动行为 0),新文本写明该断言是永久的、不限于任何一张卡,并记下上面那次消融。


    Generated by Claude Code

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

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions