Repository navigation
One artifact, N packages: let a release bundle carry co-owning packages so a product can be split into modules without renaming objects #14122
Description
从下游应用提出(steedos-labs/hotcrm-heimao、objectstack-ai/hotcrm)。全部结论实测于 @objectstack/* 17.2.0 运行实例,非推断。
修订 1:补入 §5.6「安装时对象名唯一性检查」——放宽命名空间闸的安全前提,初版遗漏;并新增 §6 兼容性一节(对存量客户元数据应用的影响)。
一、业务需求
1.1 标准产品 + 客户定制,今天靠整仓复制
我们有标准 CRM(hotcrm),客户会在它上面做定制。今天的模式是把整个仓库复制一份随便改——黑猫(hotcrm-heimao)就是 hotcrm 的 fork。
fork 税是实测的,不是担心:把 @objectstack/* 从 17.1.0 升到 17.2.0,两个仓库各做一遍,同样的坑(sharing 播种、StackBlitz 双锁文件门)各踩一遍,知识靠人手搬。黑猫对标准 CRM 的改动其实约 95% 是增量——碰到标准对象的 6 个文件是 +1,000/−15 行,15 行删除全是替换而非删能力。也就是说:绝大部分本可以是"扩展",却因为没有边界而变成了"分叉"。
1.2 产品自身的模块也在扩张
不只是客户定制。hotcrm 想在销售模块上扩 CPQ;黑猫加了"订单与毛利测算"(12 个对象 + 8 个 hook)。今天这些都堆在同一个 src/ 里,和标准 CRM 的对象平铺在一起。
1.3 由此产生的三个可度量的业务痛点
① 维护者看不清自己的系统。 Studio 是元数据管理界面,作用域是软件包。黑猫这一个包里:
| Studio 分区 | 内容 | 是否分组 |
|---|---|---|
| 数据 | 30 个对象 | ❌ 平铺 |
| 自动化 | 29 条 flow | ❌ 平铺,且标签截断 |
| 界面 | 导航树 | ✅ 有分组 |
| 权限 | 30 对象 × 9 列 CRUD × 6 个权限集 | ❌ 平铺 |
自动化那一栏里,29 条 flow 有两条都截断成 Case Escalation…,肉眼分不出是哪两条。权限矩阵是一张 30 行 × 9 列的网格,把 客户/联系人/商机 和 运费标准/等级政策/工厂成本 混在一起——给一个销售主管配权限,要在这张表里滚完 30 行。
「界面」分区之所以有分组,不是因为它做得更好,而是因为它渲染的恰好是唯一被作者写下来的那份分组(app.navigation 的 group 节点)。对象、flow、权限集在 spec 里都没有任何分组键:ObjectSchemaBase 的顶层只有 name / label / pluralLabel / description / icon / isSystem;FlowSchema 只有 name / label / description。
② 上下文预算没有模块边界。 产品定位是"一个 CRM 销售模块能完整装进 AI 的上下文窗口",并由 scripts/check-source-token-ratchet.mjs 机械守着。黑猫当前:
business semantics 112,746 / 115,000 headroom 2,254
interaction layer 44,495 / 48,000 headroom 3,505
authored total 178,417 / 180,000 headroom 1,583 ← 卡这里
没有模块边界,就没有分模块的预算——所有模块一起顶到同一个天花板,谁也说不清是谁把预算吃掉了。
③ 商业边界不存在。 CPQ 希望能单独售卖,但它和销售模块在同一个包里,没有可售的单元。
二、为什么三条显而易见的路都走不通(实测)
2.1 给对象/flow 加 module 标签 —— 预算不允许
ratchet 计入 190 个授权文件。每处 module: 'order_margin', 约 25 字符 ≈ 6 tokens,190 处 ≈ 1,100–1,200 tokens,占总量剩余额度的 70–75%。而抬 ceiling 是维护者的 floor item,不能由 agent 动。
一个纯分组键吃掉四分之三的剩余预算,性价比不成立。
2.2 拆成各自命名空间的独立包 —— 要给每个对象改名
SchemaRegistry.installPackage()(ADR-0048 §3.2):
if (manifest.namespace && !isShareableNamespace(manifest.namespace)) {
const conflictOwner = getNamespaceOwners(ns).find(o => o !== manifest.id);
if (conflictOwner) throw new NamespaceConflictError(...)
}判据是**"不是同一个包 id"**,豁免只有 RESERVED_NAMESPACES = {base, system} 加 sys。加上 defineStack 的 validateNamespacePrefix 要求 object.name === ${namespace}_${shortName}——N 个包就是 N 个命名空间,就是把 crm_account 改成 sales_account。表名、API 路径、公式、筛选器、集成、客户存的视图全断。
ADR-0048 §5 自己把这条列为已接受的非目标:"Namespace rename-on-install is an explicit non-goal for now (deep rewrite of every object name, cross-reference, and formula)."
2.3 用现成的 composeStacks 合并编译 —— 子包身份会丢
composeStacks 的关键选项:
manifest: z.union([z.enum(['first', 'last']), z.number().int().min(0)]).default('last')它在 N 份 manifest 里挑一份,扔掉其余的。 现成的合并机制是"压平",不是"保留"。走完整个重构会落回一个 30 对象的平包——收益恰好在它该出现的地方蒸发。
三、最终方案:一个发布物,物内 N 个包
编译按包,产物仍是一个 JSON,JSON 里保留 N 份 manifest,装载时按依赖拓扑序注册 N 个包。
HotCRM 发布物(一个 JSON,一个版本号,应用市场照旧发一个文件)
├── app.objectstack.hotcrm type: app ← 唯一的消费者单元,拥有应用与导航
├── app.objectstack.hotcrm.cpq type: module ← 共用 crm 命名空间,navigationContributions 注入导航
└── app.objectstack.hotcrm.order type: module ← 同上
3.1 为什么这样不违反 ADR-0019
ADR-0019 D2 已经分好层:app 是消费者单元,plugin/module/driver 是"内部贡献——.app bundle 里的框架,随 App 一起装,消费者从不单独浏览或安装"。D4 更是原文写着 "An App declares and owns a set of namespaces"——是 set,不是单个。
所以消费者仍然装一个、开一个、卸一个,D1/D2/D3 原样成立。validateSingleApp 也不咬——它只约束 type: app。
⛔ 不能跨的线:这一层必须停在控制面。一旦让"发布物"成为消费者要浏览、要安装、要卸载的东西,那就是 ADR-0019 D3 点名禁掉的 suite,苹果那段论证原样适用。
3.2 共用命名空间的所有权证明就是"同一个物"
同一个物里来的包,是同一个发布者原子交付的。物本身就是共同所有权的声明,因此安装闸的判据从"同一个包 id"改成"同一个物内"即可,manifest 不需要新增 owner 字段。
(owner 字段只在 CPQ 哪天单独成物、单独售卖时才必须——那时它要向外声明"我和 HotCRM 同属一个发布者"。ADR-0048 的 2026-08-08 addendum D2 已经把发布侧的保留权定给了 publisher,届时两侧对齐即可。本 issue 把这件事推迟,不前置。)
四、实测支撑:注册层本来就是多包的
已经支持、不用改的:
| 事实 | 位置 |
|---|---|
registerApp(manifest) 是按包函数:用该 manifest 的 id/namespace 给每个对象打 'own' |
objectql/src/engine.ts:4745 |
命名空间存储本来就是多所有者(namespaceRegistry 是 namespace → packageId 集合的 Map),getNamespaceOwners() 返回数组 |
objectql/src/registry.ts:1456 |
| 卸载已经按包清,不是按命名空间扫 | registry.unregisterObjectsByPackage(packageId) |
对象已有逐条 owner:contributors[{ packageId, ownership: 'own' | 'extend' }] |
同上 |
| 这个装机今天就注册着 23 个包,全走同一条路径 | /api/v1/meta/package |
编写侧今天就通过: 子包声明 namespace: 'crm'、定义 crm_* 对象、用 navigationContributions 注入 crm_enterprise,三种 type 全部 defineStack ACCEPTED(plugin / module / app)。唯一的闸在 installPackage。
跨包能力矩阵(逐条实测):
| 跨包做什么 | 结果 |
|---|---|
| lookup / 关系字段指向外包对象 | ✅ ACCEPTED |
navigationContributions 注入外包 app 且指向外包对象 |
✅ ACCEPTED |
本包 app 的 navigation 指向外包对象 |
❌ App 'x' navigation references object 'core_account' which is not defined in objects. |
| hook 挂到外包对象上 | ❌ Hook 'h' references object 'core_account' which is not defined in objects. |
后两条的含义很具体:导航必须走 contributions(平台自己六个插件往 setup 注入就是这个形状);拆的缝必须顺着 hook 归属切。黑猫这条缝是干净的——它新增的 8 个 hook 全部挂在它自己新增的对象上。
产物现状: os build 是 os compile 的别名,产出一个 JSON(默认 dist/objectstack.json)。黑猫已构建的那份:manifest 是单个 dict(app.objectstack.hotcrm, ns crm, v3.0.0),objects 30 个平铺,2.6 MB,manifest.engines.protocol 为 "^17.2.0"。
五、要改的(应用市场链路不动)
不管物里装几个包,市场发的还是一个文件——分发、下载、上架、安装入口的传输形态全部不变。
| # | 改动 | 规模 |
|---|---|---|
| 1 | 产物 schema:manifest 单数 → packages: [...] 列表。必须做成新增可选键、两种都读(见 §6) |
唯一实质的一处 |
| 2 | 装载处循环:objectql/src/plugin.ts:405 的 ql.registerApp(manifest) → 遍历 |
一个循环 |
| 3 | composeStacks 增加保留模式(今天 'first'/'last'/index 是刻意的挑选语义),默认值不变 |
小 |
| 4 | installPackage 命名空间闸认"同一个物内的共同所有者" |
一行判据 |
| 5 | Studio 包选择器列出 project 域的 module 包(今天只显示 app.objectstack.hotcrm,22 个系统插件被过滤) |
objectui 侧,另开 |
| 6 | 安装时逐对象名唯一性检查 —— 放宽 §5.4 那道闸的安全前提,见下 | 小,但不可省 |
5.6 为什么对象名唯一性检查不是可选项
命名空间独占今天在代理另一件事。ADR-0048 原文:
objects dodge collisions because their names are namespace-prefixed (
crm_account) and map to physical tables; a clash fails loudly at the DB
也就是说,"两个包不能共用命名空间"这道闸,顺带保证了"不会有两个包定义同名对象"。一旦按 §5.4 放宽它而不补显式检查,同一个物里两个包都定义 crm_account 时:
- 安装期不报错
- 拖到 DB 层才炸(重复建表),或者按驱动实现不同,可能一个包静默覆盖另一个包的表定义 —— 这是唯一会真正伤到客户数据的形态
所以 §5.4 与 §5.6 必须同批落地,不能拆开发布。
六、兼容性:对存量客户元数据应用的影响
结论:不拆模块的存量客户零影响——前提是 §5.1 做成增量而非替换。
6.1 核心原因:本方案不改名
这既是它相对 §2.2 的根本优势,也正是兼容性的答案:
- 对象名不变 → 表名、REST 路径、公式、筛选器、客户存的视图全部不变
- org 级元数据覆盖(ADR-0005 overlay)按对象名/字段名键控 → 不变
- 不拆模块的包仍是一个包,
_packageId不变 - Studio 路由
/_console/studio/<packageId>/...不变 - 客户不需要做任何数据迁移
6.2 逐条
| 改动 | 对存量客户 |
|---|---|
| §5.1 产物 schema | 前提:新增可选键、两种都读(有 packages 就遍历,没有就把 manifest 当单元素列表)。这样存量产物行为逐位不变 |
| §5.2 装载循环 | 内部,无感 |
§5.3 composeStacks |
新增可选项,默认仍是 'last' → 现有调用方无感 |
| §5.4 闸放宽 | 放宽从不打破原本能跑的东西 —— 但必须与 §5.6 同批 |
| §5.5 Studio 选择器 | UI 增量 |
| §5.6 对象名唯一性 | 新增的拒绝:只会拒掉今天本就会在 DB 层炸的配置,且拒得更早、更清楚 |
6.3 前向兼容已有机制,不用新建
产物里已有 manifest.engines.protocol(黑猫那份是 "^17.2.0")。新格式产物声明新的 protocol range,旧运行时会干净地拒绝而不是错误解析。
6.4 建议的验收条件
存量单
manifest产物经新装载路径后,注册结果逐位相同(包记录、对象 FQN、_packageId标记、命名空间所有者集合)。
这条应当写成测试,而不是靠 review 判断。
七、待决策
① 这条路走不走。 这是主问题。替代方案是 §2 的三条,各自的否决理由已实测列出。
② 拓扑序是必须项,不是优化项。 一个包用 defineObjectExtension 扩另一个包的对象,必须在被扩的包之后注册。今天跨物安装靠 manifest.dependencies → resolvePluginOrder 定序;N 个包塞进一个物之后,装载处必须拓扑排序,不能直接 for 数组。这是这套改动里唯一会静默出错的地方——顺序错了不报错,只是扩展没生效。请把它作为验收条件,不要留给实现者判断。
③ 一个物一个版本号,取舍认不认。 好处:版本矩阵直接消失,客户不可能装出 core 3.2 + cpq 1.4 这种没人测过的组合,defineObjectExtension 也不会因为被扩的包单独升级而悬空。代价:不能单独热修一个模块,任何一处修都要重发整物。对内部模块这个取舍是对的;对要单独售卖的模块不成立,那种应当独立成物。
④ 产物体积。 黑猫 30 个对象已经 2.6 MB。模块继续往一个 JSON 里加,市场传输和启动解析都会涨。不是拦路虎,但该在 schema 定下来的时候就决定要不要分段加载,而不是等它到 20 MB 再回头改格式。
⑤ 确认 owner 字段推迟。 本方案不加 manifest 的 owner/publisher 字段,理由见 §3.2。如果维护者认为应当与 ADR-0048 addendum D2 同批落地,请在此说明——那会让 §5 多一项。
八、下游可以先做什么(不阻塞本 issue)
黑猫和 hotcrm 的交付链路在我们手里,可以先按包拆源码结构、锁步发布(N 个包永远同版本、整批安装),等产物 schema 落地再切过去,源码结构不用返工。
如实标注这条中间路的缺陷:锁步是约定,不是闸——没有任何机制阻止装成错配的一对。对自建交付可控,一旦上架市场就不成立。这正是本 issue 要解决的部分。
Activity
navigationContributions[].group语义、权限集的模块归属、dashboard/report → dataset 跨包绑定(hotcrm 分拆方案挖出的 4 个上游缺口) #14454hotlong commentedon Sep 2, 2026
进度:#14375 落地(PR #14430 07:59 合并);objectui#7331 在队列;hotcrm 分拆方案待维护者合
GET /packages与GET /packages/:id每行携带服务端自己的可写判定writable(isWritablePackage),Studio 不再用scope启发式反推 #14375 · PR feat(packages): GET /packages and GET /packages/:id rows carry the server's own writable verdict (isWritablePackage) #14430 合并 → 6/8。真实进程取证(app-todo)与既有缺陷GET /api/v1/packages//api/v1/packages/:id//api/v1/meta/package全部 500「Converting circular structure to JSON」—— 栈里带插件实例(showcase)时,包记录存的是含实例的原始摊平 bundle #14442 的记录见 PR 正文。packages[]发布物 ——os build认识项目级composeStacks('preserve'),装配后的包体有自己的声明(#14242 取 B) #14439 落地后由 PM 复跑(已记在 feat(cli+spec): 从 N 个软件包的作者态编出一个packages[]发布物 ——os build认识项目级composeStacks('preserve'),装配后的包体有自己的声明(#14242 取 B) #14439)。packages[]发布物 ——os build认识项目级composeStacks('preserve'),装配后的包体有自己的声明(#14242 取 B) #14439(多包编译路径):agent 在飞;已同步两条输入——三道 parse 缝(作者态defineStack/ 编译compile.ts:271/ 装载metadata/plugin.ts:912)与 feat(packages): GET /packages and GET /packages/:id rows carry the server's own writable verdict (isWritablePackage) #14430 已进 main(夹具可直接断言writable:false)。packages/PKG/src/规则)。方案第一刀建议cpq。GET /api/v1/packages//api/v1/packages/:id//api/v1/meta/package全部 500「Converting circular structure to JSON」—— 栈里带插件实例(showcase)时,包记录存的是含实例的原始摊平 bundle #14442(栈带插件实例时包读门 500,既有缺陷)、POST /packages/:id/duplicate对一个启动中的代码包返回success:false, copiedCount:0, failedCount:0(空 base)—— ADR-0070 D4「复制成可写 base」对代码包是否本就该拷贝其对象? #14451(对代码包duplicate得到空 base,待 triage)、ADR-0130 跨包规则矩阵补全:9 类命名对象的元数据、navigationContributions[].group语义、权限集的模块归属、dashboard/report → dataset 跨包绑定(hotcrm 分拆方案挖出的 4 个上游缺口) #14454(分拆方案挖出的 4 个跨包规则缺口,其中权限集归属需维护者决定)。三张都不挡装载链,是否挡发版由维护者定。发版前清单(维护者指令:平台侧齐了再发)
writable.objectui-sha提针Generated by Claude Code
hotlong commentedon Sep 2, 2026
进度:objectui#7177 落地(PR objectui#7331 08:00 squash 合并)→ 7/8
Studio 切换器现在读服务端
writable,缺失回落旧启发式;多包 Studio 复跑挂 #14439 落地后。发版前清单第 4 项剩.objectui-sha提针(方式待维护者答复)。Generated by Claude Code
hotlong commentedon Sep 2, 2026
维护者六项决定(2026-09-02)与执行
salespackages/<x>/src/规则priority:p1、已派发(方向 A:注册载荷剥掉运行时实例).objectui-sha提针#14454 第 1、2、4 项(9 类命名对象的跨包矩阵、
navigationContributions[].group语义、dashboard/report → dataset 跨包绑定)为测量与 pin,已认领派发,不挡发版。发版前清单(更新)
writable(PR #14430).objectui-sha提针Generated by Claude Code
hotlong commentedon Sep 2, 2026
进度:hotcrm 分拆方案落地(hotcrm#1449 09:12 squash 合并,含三项决定的记录)
hotcrm 侧的设计交付完成:
docs/architecture/module-split-plan.md+module-split-inventory.json进 main,"Decisions recorded (2026-09-02)" 一节记下 R4 取 A、AGENTS.md 路径规则等 #14439、权限集本次 B / 二期 A(#14488)。代码分拆等 #14439 + 发版 + hotcrm bump。Generated by Claude Code
packages[i]body carry the same definitions #14512test(objectql): measure the ADR-0130 cross-package matrix over nine o…
hotlong commentedon Sep 2, 2026
§4 跨包能力矩阵 —— 补全(#14454 第 1、2、4 项,可直接折进正文 §4)
来源:#14454,PR #14549(draft,tests-only,22 条 pin)。方法与 §4 相同:两包同物共命名空间,经真实装载路径
manifest.register({ packages: [...] })装进 bootedObjectKernel(ADR-0130 D1 / #14354 的ArtifactInstallScope);A 拥有crm_account,B 从被测项类点名它。§4 记了四条判定但没记是哪道门给的,而它们不是同一道:
defineStack→validateCrossReferences(packages/spec/src/stack.zod.ts:1109)。它只看得见一个 stack,所以「co-owner 的对象」与「拼错」在它眼里无从区分。§4 那两条 REFUSE 的报错原文正是这里的,逐字。registerApp→installPackage(packages/objectql/src/engine.ts:4797)。它对下列任何项类的对象引用都不校验,一律经registerMetadataCollections注册。⇒ 只读装载门会得到九类清一色 ACCEPTED(真,但无用);只读编写门会漏掉「运行时什么都不管」。故下表判定取实效判定:编写门拒 = 模块根本写不出来,装载门怎么想都不影响这个结论。
矩阵(
file:line在packages/objectql/src/registry-cross-package-item-classes.test.ts)navigationContributions注入外包 app 且指向外包对象navigation指向外包对象App 'x' navigation references object 'core_account' which is not defined in objects.Hook 'h' references object 'core_account' which is not defined in objects.:236连续性对照,本轮以同一方法复现objectNameAction 'NAME' references object 'OBJECT' which is not defined in objects.:282/:316data.objectView[0].list references object 'OBJECT' which is not defined in objects.:335object:384/:434regions[].components[].dataSource.object:448object:392/:434object:410/:434objectsPermission 'NAME' grants on object 'OBJECT' which is not defined in objects.:290/:316objectSeed data references object 'OBJECT' which is not defined in objects.:298/:316targetObjectMapping 'NAME' targets object 'OBJECT' which is not defined in objects.:306/:316config.objectName)config是开放z.record):477dataset(第 4 项):529dataset(第 4 项):551新测九类:5 拒 / 7 受(含第 4 项两条)。
crm_nowhere/ds_nowhere),所以这是一条发现,不是绿灯。第 4 项结论
一个模块可以自带自己的 dashboard。 dashboard widget 与 report 绑定别的包定义的 dataset,两道门都接受;pin 同时读回 B 的 widget(
_packageId= B、dataset名完好)与 A 的 dataset(_packageId= A、object= A 的对象)。第 2 项:
navigationContributions[].group(packages/objectql/src/registry-nav-contribution-group-semantics.test.ts)group按目标 app 声明的 group 节点解析id与type === 'group',追加进该 group 的 children,不在顶层重复:143、:158:170、:186、:245priority定序而非注册顺序:270、:284🔴 命题 2 需要维护者/PM 拍板 —— 平台做的是第三件事:把贡献「上提」到 app 顶层。
不消失,也不失败:无 throw、无
code/status、无console.warn、无console.error(:186断言这两个通道都为空)。唯一痕迹是 registry 私有log()的一行console.log,按logLevelinfo/debug开关。出厂默认是info,所以默认启动会打一行;但在warn——正是OS_REGISTRY_LOG用来选的档,也是本包自己vitest.config.ts选的档(#13517)——relocation 照做,一个字不出。编写门也拦不住(:245):navigationContributions不在任何 cross-reference 规则里,拼错的 group id 两道门全过。这比方案担心的「静默丢弃」更糟。 丢了会有人发现;被上提的条目在场、看着正常,而信息架构已经变了 —— 那恰是 17 个导航节点转换要保住的唯一属性。
:158另证同一条路径也会被一个拼写正确但指向object类型节点(而非group)的 id 触发。⛔ 按卡片围栏,本卡不改运行时行为,pin 只记录今天的行为。选项与四轴推荐已写在 #14454 的 os-dev-report(推荐 B:把那行
console.log升为console.warn,并让 hotcrm 转换自带一条 IA 断言;A「装载门拒绝」会引入 registerAppNavContribution 今天刻意没有的装载顺序约束)。🟡 顺带一条:编写门没有 ADR-0112 信封
五条 REFUSED 全是
defineStack抛的裸new Error(...),code与status都是undefined——所以「拒了,什么 code」的答案是:没有 code。报错 message 是区分不同拒收的唯一判据,故 pin 断言 message,并在:251单独把信封的缺席也钉住(转红 = 信封来了,届时改 pin 与本表,⛔ 不是删断言)。与 #14367(registerObject裸 Error)同形,隔一道门。Generated by Claude Code
navigationContributions[].groupthat names no group in the target app is silently RELOCATED to the top level — refuse, warn, or leave to the consumer? #14553hotlong commentedon Sep 2, 2026
§4 fold-in — cross-package matrix measured (#14454 → PR #14549, merged)
Two pin files now on
mainextend the §4 table; every row reads both doors — the authoring gate (defineStack→validateCrossReferences, which sees one stack and cannot tell a co-owner's object from a typo) and the install gate (registerApp→installPackage, which validates no object reference on any of these classes). Method as §4: packages A and B installed as same-artifact co-owners of one namespace throughmanifest.register({ packages: [...] })on a booted kernel; A ownscrm_account, B names it.Item 1 — object-naming classes (
packages/objectql/src/registry-cross-package-item-classes.test.ts):objectNamedata.objectobjectsobjecttargetObjectobjectregions[].components[].dataSource.objectobjectobjectconfig.objectNamez.record)5 REFUSED / 5 ACCEPTED. REFUSED rows carry the local-object control (same fixture, object declared locally → accepted); ACCEPTED rows carry the dangling-name control and read the item back (
_packageId= B,resolveObject= A's definition,getNamespaceOwners('crm')= both). §4's R4 (hooks[].object) is re-measured with this method and reproduces its message verbatim.Item 4 — analytics binding: a dashboard widget and a report in B binding a dataset defined in A are ACCEPTED at both doors and resolve. A module can carry its own dashboard.
Item 2 —
navigationContributions[].group(packages/objectql/src/registry-nav-contribution-group-semantics.test.ts): proposition 1 holds (resolves against atype: 'group'node the target app declares; an id that names a non-group node is not a match); proposition 3 holds (ordered bypriority, stable tie-break to arrival, schema default 200). Proposition 2 does not hold as posed: a contribution into a group the app does not declare is neither dropped nor refused — it is relocated to the app's top level, with no throw, nocode/status, noconsole.warn; the only trace is oneconsole.loggated atinfo, silent atOS_REGISTRY_LOG=warn. The authoring gate cannot see it either.Item 3 stays as ruled: option B, permission sets whole in the
type: apppackage (#14487 / PR #14498, addendum for the maintainer's hand-merge; #14488 for phase 2).Routed: #14553 (needs-user-decision) for the silent relocation — options A refuse / B warn / C consumer-side test, recommendation B now + C in hotcrm; #14552 (bug) for the five REFUSED refusals carrying no ADR-0112 envelope, same class as #14367 / #14474. Neither gates the release.
Generated by Claude Code
hotlong commentedon Sep 2, 2026
Landing + post-landing verification (PM seat)
packages[]发布物 ——os build认识项目级composeStacks('preserve'),装配后的包体有自己的声明(#14242 取 B) #14439 landed — PR feat(cli+spec): compile a project of N packages into one packages[] artifact, with the assembled package body declared (ADR-0130 D4 producer, #14242 B) #14513 merged 13:46 UTC (with ArtifactPackageEntrySchema's body half cannot describe the payload the load path actually registers #14242 road B). The producer side of D4 is onmain:composeStacks(…, { manifest: 'preserve' })assembles per-package bodies,os buildwritespackages[], the load gate parses each entry againstAssembledPackageBodySchema. Two reviewer commits on the way in fixed a declaration-bundling regression (a named alias in the stack schema's printed type pulled the whole stack declaration into every@objectstack/spec/systemconsumer and OOM'd the debt re-measure); the resulting static-type narrowing is recorded on the PR, and the ceiling headroom it exposed is finding(tooling): theqa/http-conformanceTEST_DEBT re-measure already needs ~4.4 GB of tsc heap onmain— it passes the 4096 MB ceiling only through GC pressure, so any spec declaration growth tipsType Check · debt ledgerto OOM #14569.examples/app-multi-package(recorded on feat(cli+spec): 从 N 个软件包的作者态编出一个packages[]发布物 ——os build认识项目级composeStacks('preserve'),装配后的包体有自己的声明(#14242 取 B) #14439): the three server doors are right — both rowswritable: false, moduledependencies → core, per-object_packageIdstamps, namespacecrmowned by both. Studio lists both packages read-only and renders the cross-package lookup. But per-package grouping fails for the App package:crm_orderis served twice onGET /api/v1/meta/object, appears undercom.example.multi.core, and the layers door names core as its owner while the item door names orders — the metadata artifact door stamps the flattened top-level copy with the artifact'smanifest.id. Filed as Multi-package artifact: the metadata service attributes every top-level object to the artifact'smanifest.idwhile the registry owns it per package —crm_orderis served twice onGET /api/v1/meta/object, listed undercom.example.multi.core, and Studio's Data pillar for the App package shows the module's object #14599 and dispatched now; it is the A multi-package artifact serializes its metadata twice — the flattened top level and everypackages[i]body carry the same definitions #14512 edge made concrete, so A multi-package artifact serializes its metadata twice — the flattened top level and everypackages[i]body carry the same definitions #14512's producer half stays with the maintainer (recommendation B) while the reader half proceeds under Multi-package artifact: the metadata service attributes every top-level object to the artifact'smanifest.idwhile the registry owns it per package —crm_orderis served twice onGET /api/v1/meta/object, listed undercom.example.multi.core, and Studio's Data pillar for the App package shows the module's object #14599.scopekey" property never reaches the served row —defineStackmaterialisesscope: 'project', so the fixture cannot separate the serverwritableverdict from the clientscope !== 'project'heuristic it claims to #14597 (fixture note): the module's "noscopekey" property does not survive materialisation (ManifestSchema.scopedefaults toproject), so the fixture cannot by itself discriminate the server verdict from the old client heuristic. Not gating.Release checklist, platform side: #14599 (this feature's own defect — one owner per object across doors, Studio grouping) and #14499 (#14309's fix, another seat, conflicted with
mainand idle since 09:34). Everything else in the original eight cards is onmain; #14498 is the ADR addendum awaiting the maintainer's hand-merge and does not gate.Generated by Claude Code
hotlong commentedon Sep 2, 2026
#14599 landed — post-landing verification on
mainPASSES (PM seat)packages[]artifact per package;resolveArtifactPackageOrdernow lives in@objectstack/coreand both readers share it — D5 as one function, D7 pinned as a literal register sequence). Contract review PASS with the same-session disclosure on the PR; Clause-② NO.main655b106c(fresh worktree, real boot ofexamples/app-multi-package, recorded on Multi-package artifact: the metadata service attributes every top-level object to the artifact'smanifest.idwhile the registry owns it per package —crm_orderis served twice onGET /api/v1/meta/object, listed undercom.example.multi.core, and Studio's Data pillar for the App package shows the module's object #14599 with JSON rows and screenshots):GET /api/v1/meta/objectserves onecrm_order(ownercom.example.multi.orders) and onecrm_account(ownercom.example.multi.core);?package=returns exactly whatGET /api/v1/packagessays each package owns; the layers door and the item door name the same owner;multi_crmis stamped core; both package rowswritable: false. Studio, measured on both the vendored console (pin67dadd60) and the objectuimainHMR console (bb459ea9, contains [approvals][qa] Browser-verify the #7213 entry convergence end to end, and refresh the approvals checklist's entry paths #7331): the App package's Data rail lists onlyAccount, the module's onlyOrder, both packages read-only in the picker and front door, and the cross-package lookupcrm_order.account → crm_accountrenders. Every defect from the 14:17 run is gone. No new issue filed (three observations recorded on Multi-package artifact: the metadata service attributes every top-level object to the artifact'smanifest.idwhile the registry owns it per package —crm_orderis served twice onGET /api/v1/meta/object, listed undercom.example.multi.core, and Studio's Data pillar for the App package shows the module's object #14599 as non-defects).ADR-0130 consequence row 6 (Studio grouping by package) now holds on
main.Platform release checklist: every card of this epic that gates the feature is on
main(#14375, #14439, #14454, #14599; #14442 closed as a duplicate of #14309). The one open gating item is #14499 (#14309's fix, another seat's PR — its queue runs were green at 17:45; landing is that seat's). Not gating: #14498 (ADR addendum, maintainer hand-merge), #14512 producer half (needs-user-decision, rec B), #14553 (needs-user-decision, rec B+C), #14569 (needs-user-decision, rec A measured), #14597 (fixture note), #14552 (envelope bug).Generated by Claude Code
hotlong commentedon Sep 2, 2026
Platform release checklist for this epic — clear (PM seat, 20:24 UTC)
PR #14499 (#14309, the package-record serialization fix that #14442 duplicated) merged at 20:21 UTC. With #14513 (#14439) and #14643 (#14599) already on
mainand verified end-to-end onexamples/app-multi-package, nothing on the platform side of this epic gates the release any more.Still open, none gating: #14498 (ADR-0130 addendum, governed — maintainer hand-merge), #14512 (producer half,
needs-user-decision, recommendation B), #14553 (needs-user-decision, recommendation B + C), #14569 (needs-user-decision, recommendation A with a measured ceiling), #14552 and #14597 (findings). The hotcrm module split (hotcrm#1449 plan) starts against the released version that carriespackages[].Generated by Claude Code
hotlong commentedon Sep 4, 2026
进度 — 平台半又落两张,读取程序已启动(2026-09-04 05:14Z)
PM 席,session
session_01UHvF5hyiZjnceExFnfQB8m的接续会话session_01UHvF5hyiZjnCyExFnfQB8m。维护者 04:38 把两张 PR 同时入队,05:08 / 05:14 相继合入。刚落地
warn()活过OS_REGISTRY_LOG=warn;os build与os validate在编译期回答同一问题,结果走已声明的warnings键--json形状改过一次,值得记:第一版新增顶层键navigationGroupDiagnostics,被 #11727 / #11643 两条既有 pin 挡下(「advisory 填已声明的键,不是新开一个面」)。改走warnings后又触发第三条 pin——build 与 validate 的载荷必须一致——所以os validate也算同一份结果,这本身是更对的答案。没有为了变绿改动任何一条 pin。#15004 探针的形态,后续三张卡都受它约束
我在卡面写的验收是「option-B 腿今天必须红」,照字面做这个 pin 会一直红到整个程序完成、根本合不进去。实施席换成集合相等的 shrink-only 台账
OPTION_B_LOSSES(24 行):今天丢 24 处、台账恰列这 24 处所以绿;修好一处读取方 → 集合不等 → 红 → 必须删台账那一行;回归、出现未枚举的读取方、或探针自己量少了,同样红。区分度以清空台账反向验证(清空后报 24 个子系统丢集合)。这比我原来的要求好,记在这里而不是默默采纳。另一条设计约束:没有一行是把被测读取重写一遍——每行要么调仓库真出货的 reader,要么启真
LiteKernel带真AppPlugin记录它交给子系统的东西。照抄形状的行在那处读取修好后会继续红,然后被删掉。在飞
#14512 裁定 B(重新定范围为程序,评论 5528589044)后立的三张读取卡已并行派出:#15005 runtime · #15006 cli · #15007 plugin-security。三张均
Blocked-by: #15004(已解除),artifact 在整个读取半保持 additive;#14512 作为发射半留在pm:blocked。serve.ts:2632/:2669的 from-source 腿、dev.tsreadArtifactObjects、compile.tsunion authoring-rule)——「探针对 serve.ts 有行」只真了一半,产物腿覆盖、from-source 腿没有;#15007 要求按形状而非按包名再扫一遍,因为两次枚举都按包名划范围、两次都漏,plugin-security那处就是漏出来的。其余
#14569 已裁 A1,归 devx 席 PR #14805。#14487 补记已于 184f6b0 落 main。发版:仍无新 tag,
@objectstack/spec仍 17.2.0;按裁决,读取程序不阻塞发版。Generated by Claude Code
os-justin commentedon Sep 4, 2026
Cross-seat notice —
domain:specseat,session_01H2oQebDDxYKfWZusyd8GXk, 2026-09-04T08:54Z. #15219 (maintainer ruling A for both keys, director relay 5537162356) is implemented on PR #15289 (packages/speconly). Its consequence in this epic's reader-half surface:packages/cli/test/option-b-reader-acceptance.pin.test.ts:188transcribes the artifact-envelope key list literally (seven keys) and now receives nine (plugins,devPluginsjoined), so Test Core is red on that PR until the literal moves;packages/cli/test/fixtures/option-b-collection-zoo.ts:26/:67say "seven" in prose for the same set. The spec seat ruled (5537508183's card, ruling comment to follow on #15219): the #15219 claim surface is extended by exactly those two mechanical edits — the two names added to the:188list, "seven" → "nine" in the zoo prose — pushed on PR #15289. Measured before ruling: four in-flight branches of this epic edit the pin file (claude/issue-15005-*,-15006-*,-15007-*,-15229-*; hunks at 83-141, 207-224, 272-284) and #15229 also edits the zoo; none overlaps 176-200 or the two prose lines, so the text merge is clean by position. If #15229 lands first with the nine-key list, #15289's merge is a no-op on those lines. No other file of this epic is touched; #14512 staysBlocked-by: #15219until #15289 merges. Objections: reply here or on #15219.Generated by Claude Code
hotlong commentedon Sep 4, 2026
里程碑 ——
OPTION_B_LOSSES账本归零,读取半收口PR #15261(#15005)于 12:52 UTC 合入
main(f1a1028ea)。在main上直接读:账本轨迹
33681eaef)B2 · plugin-security appSecurityPluginOptions over the from-source configf1a1028ea)这一点值得写明,因为「台账空了」在别处常常意味着门禁失效。这里相反:
.toEqual(ledger)),不是「不超过」。账本为空,意味着探针测量的每一行现在都必须在两种形态下present——读取半发生回归、或者一个新读取器未解析地到场,当场红,没有任何余量能吸收它。rows.length >= 0(对任何数组恒真),feat(runtime): every top-level collection read gains apackages[]path (#15005) #15261 把它重锚到实测的 30(toBe(-1)反读出 30,下限 31 在同一夹具上确实变红),并保持>=的 shrink-only 方向——探针新增行照样绿,停止测量某行才红。这补回了文件头声称的第四个方向:「探针自己悄悄少测 ⇒ RED」。resolveArtifactCollections中性化并重建dist/后,pin 精确点名 23 行,与fbecffe1d上的账本逐字节相同;恢复后 7 passed。发射半解锁
#14512 从
pm:blocked解锁。 它当初转 blocked 的理由是读取半没有收口——发射器一旦停止扁平化,任何还在只读顶层的读取器都会静默失明,而当时没有测量能证明它们全被教会了。现在有了:账本是那份测量,它是空的。⛔ 一句给发射半的提醒:解锁不等于验收标准变松。#15004 的 pin 从这一刻起是双向的——它既拦读取半回归,也会在发射半翻转后立刻显形。发射半的验收应当直接用它,而不是另起一套。
读取半六张卡
#15004(探针)· #15005 · #15006 · #15007 · #15229 · #15298 —— 加上 #15226 / #15228 两条先落地的私有解析。#15229 的 PR #15281 尚未落地,是这一族里唯一还在飞的。
Generated by Claude Code
hotlong commentedon Sep 4, 2026
读取半收口 —— 六张卡全部落地
PR #15281(#15229)于 15:13 UTC 合入
main(c550bafc2),是这一族最后一张。@objectstack/runtime@objectstack/cli四个配置加载边界@objectstack/plugin-security@objectstack/verify+plugin-devmain上的最终读数:下限从 30 变成 35,而且是零余量的 —— 这一点值得单独记
#15261 落地时下限是实测的 30,当时是对的。但 #15281 的席位在合并时发现:那棵树上探针实际测 31,下限 30 留了一行余量 —— 意味着一行悄悄不再被测量,pin 也不会红。
它把新下限设成合并后探针的精确行数 35,没有余量。⚠️ 这不是我在派发单里要求的东西(我只要求"往上抬"),是它自己发现并收掉的。文件头声称的第四个方向是「探针自己悄悄少测 ⇒ RED」;有余量时那句话就有一行的水分,现在没有了。
给以后审这类反空转下限的人:要问的不是「这个数是不是测出来的」,而是「它和当前实测值差多少」。 一个测出来的数,放久了也会变成余量。
账本归零之后,这张 pin 是双向的
.toEqual(ledger)),不是「不超过」。账本为空意味着探针测的每一行都必须在两种形态下present—— 读取半回归、或新读取器未解析地到场,当场红,零余量吸收。resolveArtifactCollections后 pin 精确点名 23 行,与fbecffe1d上的账本逐字节相同。发射半
#14512 已从
pm:blocked解锁(#15261 落地时)。⛔ 一句提醒不变:解锁不等于验收变松。#15004 的 pin 从此双向 —— 它既拦读取半回归,也会在发射半翻转的那一刻立即显形。发射半的验收应当直接用它,而不是另起一套。Generated by Claude Code
defineStack's per-stack cross-reference validation refusespermissions[].objectsanddata[].objectthat name an object owned by ANOTHER PACKAGE OF THE SAME ARTIFACT — so ADR-0130's co-owned artifact and the "permission sets stay whole in the app package" addendum cannot both hold once the app package owns objects #18202os build/os devper-package author-time rules refuse an action param's record-pickerreferenceto a dependency's object (object-reference-unknown), while the composed pass accepts it and ADR-0130 R1 accepts the field-level equivalent #18204os-elon-musk commentedon Sep 14, 2026
Three more sub-issues from the first real downstream split (2026-09-14)
objectstack-ai/hotcrmis executing the ADR-0130 split this record proposed. The layout landed (hotcrm#1910) and the packaging (hotcrm#1907) is built and runtime-proven on the 17.4.0 pin — two packages registered, ownership correct (15 objects app + 3 module), the 33-node navigation identical, an admin browser walk identical to main, seed replay resolving across the package boundary. It cannot land, because three author-time rules read one package of a co-owned artifact as a closed world:defineStackper-stack cross-reference validation refusespermissions[].objects/data[].objectnaming a declared dependency's objectos build's per-package author-time pass refuses an action param's record-pickerreferenceto a dependency's object, while the composed pass accepts ittranslation-target-unknownreadsapps[].navigationonly, so every locale key for anavigationContributionsitem is a false positive whose advice deletes a working translation#18202 is the one that makes two accepted records contradict each other: §5 of this issue made the artifact co-owned, and the 2026-09-02 addendum (#14487) rules that permission sets stay whole in the
type: apppackage. That addendum was measured on hotcrm#1449 against an app package declaring no objects at all — exactly the casevalidateCrossReferencesearly-returns on (if (objectNames.size === 0) return errors;). The moment the app package owns one object, the check fires and the addendum's shape is unauthorable. Either the check becomes dependency-aware, or the addendum needs re-ruling and #14488 needs deciding; #18202 asks for that decision explicitly.Two sibling gaps from the same split are already filed and are not blockers: #18170 (the ADR-0046 package-docs collector reads only
<config dir>/src/docs, so a moved docs directory is a silent loss on an exit-0 build) and #18171 (objectstack.config.tsmay carry no named export — an undocumented config-authoring constraint).#18202 and #18204 are dispatched this round under the maintainer direct-dispatch channel (authorization quoted on each card); they are claimed by the hotcrm epic PM seat for dispatch and review only — landing belongs to the
domain:specanddomain:cliseats, and both PRs will carryneeds:contract-review.Generated by Claude Code
translation-target-unknownreadsapps[].navigationonly, so every locale key for a CONTRIBUTED navigation item (navigationContributions) is a false positive whose advice deletes a translation the runtime honours #18203test(cli): hold field-level and action-param object references in agr…
fix(pm): bind H31's carrier pair through the closing keyword (objects…
fix(spec): resolve permission-set and seed object references against …
test(spec): the second root-level test file group's titles state each…
objectstack-fleet commentedon Oct 18, 2026
Closed
completed: all 16 sub-issues have landed · triage seat ·session_01AavokzJ5DndAwitDXvKy4U· 2026-10-18T14:40ZTriage seat (objectstack-wide, seat post #6015). ⛔ Not a claim. Closed on the maintainer's order in this seat's chat on 2026-10-18: 「epic 处理完的,你判断可以关闭的就关闭」.
Blocked-by:line.Generated by Claude Code