Skip to content

全仓唯一一条 PageNodeSchema['type'] === 'page' 的钉扎,住在一个已不引用它的包里 #3227

Description

@os-zhuang

在 objectui#3223(删除 @object-ui/layout 的死渲染器 PageNodeRenderer)验收时记下的后续,不在该 PR 处理(超出那单的 packages/layout/ 范围)。

现状

packages/layout/src/__tests__/spec-symbol-batch7.test.ts 里有一条编译期断言,钉住 spec 的页面节点判别式:

type _NodeIsATaggedNode = Assert<Equal<PageNodeSchema['type'], 'page'>>;

它是全仓唯一一条钉这件事的断言,而它守的那个键正是 @object-ui/components 的 PageRenderer 注册的键。#3223 删掉 PageNodeRenderer 时刻意保留了它:被删的是组件,活下来的是 wire 契约。

问题

保留是对的,但它现在的住处不对:@object-ui/layout 已经完全不引用 PageNodeSchema 了。于是这条断言处在一个尴尬位置——

  • 它出现在一个名为 spec-symbol-batch7(一次已完成的改名批次)的测试文件里,文件名说的是历史,不是它现在的职责;
  • 下一个清理 packages/layout 测试的人,看到一个不引用 PageNodeSchema 的包里有一条关于 PageNodeSchema 的断言,合理的反应是"这是上一轮重构的残留",然后删掉——全仓对这个 wire 判别式的唯一保护就没了,且没有任何东西会报红。

这就是"守卫本身没有守卫"的形态:一条覆盖不会被测试发现其消失。

建议

搬到 packages/types(它 re-export spec 的类型,是这条断言的自然归属),或搬到 @object-ui/components 里 PageRenderer 注册点旁边(它是这个键的实际拥有者)。两处都比现在合理;倾向 packages/types,因为这条钉的是类型契约而非某个渲染器的行为。

搬迁时把注释写清楚它钉的是什么、为什么不能删——现在 packages/layout 那份注释已经被 #3223 改写过一轮("重命名的对象已被删除,活下来的是分层拆分本身"),搬过去要再改一次,让它对新住处成立。

不要在搬迁中顺手放宽或删除这条断言。 若发现别处已有等价覆盖(我查的时候没有),核实后再决定是否合并两处。

关联:objectui#3223、objectui#3161 / objectstack#4115 batch 7。

Activity

  1. xuyushun441-sys commented on Aug 3, 2026

    @xuyushun441-sys
    Contributor

    认领:PM 循环第 1 轮
    会话:session_01NVPjPzmmAJ2Ngtvgg5MSRa(objectui 分片 PM,账号 xuyushun441-sys,已在 objectstack#4604 登记)
    分支:claude/issue-3227-move-pagenode-pin
    Worktree:objectui-issue-3227

    复核记录:前提成立,且「唯一性」实测属实

    origin/main @ ca58e45:

    • 断言仍在 packages/layout/src/__tests__/spec-symbol-batch7.test.ts:97(type _NodeIsATaggedNode = Assert<Equal<PageNodeSchema['type'], 'page'>>);
    • packages/layout 对 PageNodeSchema 的引用只剩这个测试文件本身(第 31/49/89/95/97 行)加一条 CHANGELOG.md 历史记录 —— 即原单说的「住在一个已不引用它的包里」成立,没有被后续提交推翻;
    • 同文件第 95 行还有一条 _DocumentIsNotNode(钉 SpecPage ≠ PageNodeSchema),它与被搬的这条是同一件事的两面,搬迁时要一起考虑,不要只搬一条把另一条留在原地。

    归属裁定(PM,可否决)

    原单倾向 packages/types,给了两个候选。按 packages/types,理由是原单那条即成立:这钉的是类型契约,而 packages/types 正是 re-export spec 类型的那一层;放到 @object-ui/components 的 PageRenderer 注册点旁边会让「类型契约」与「某渲染器行为」两件事混住,下一个读者仍要猜它为什么在那儿。

    ⛔ 硬约束(原单已写明,派发时再钉一次):不得在搬迁中放宽或删除这条断言。搬完必须 sabotage 验证 —— 故意把 spec 侧判别式改错,确认新住处的断言真的红;贴输出。


    Generated by Claude Code

  2. xuyushun441-sys commented on Aug 3, 2026

    @xuyushun441-sys
    Contributor

    派发失败记录(第 1 轮)—— 认领保留,零代码产出

    派给本单的开发 agent 在动任何文件之前就被掐断,原因是账号级会话配额:

    Agent terminated early due to an API error:
    You've hit your session limit · resets 10:30am (UTC)
    

    它最后的动作是「开始读 AGENTS.md」,此后没有任何写操作。

    当前真实状态(请以此为准,不要按「已派发」理解)

    项 状态
    分支 claude/issue-3227-move-pagenode-pin ❌ 不存在(远端已核)
    Worktree / 提交 / PR ❌ 无
    代码改动 ❌ 零
    认领 ✅ 保留(session_01NVPjPzmmAJ2Ngtvgg5MSRa)

    pm:dispatched 标签保留的含义是「本轮已派过且失败」,不是「有活在飞」。

    为什么保留认领而不是退回队列

    SKILL.md 的僵尸认领回收条件是「认领超 ~24h 且分支不存在、无 PR」。本条才几分钟,且失败原因与本单内容完全无关(配额,不是任务有问题)。退回队列只会让下一个扫描者重做我已经做完的复核 —— 上面那条前提复核(断言仍在 layout 第 97 行、该包已不引用 PageNodeSchema、同文件第 95 行还有一条 _DocumentIsNotNode 需一并考虑)仍然有效,重派时直接用。

    重派条件

    配额 10:30 UTC 恢复后重派,派发内容不变(目的地 packages/types;⛔ 不得放宽或删除断言;必须 sabotage 验证,并先确认目的地包真的对测试文件做类型检查 —— 参见 objectstack#4642 记录的「编译期 pin 空转」形态)。

    若在此之前有其它会话想接手:请先在本单评论声明,我立即让行并撤掉认领。


    Generated by Claude Code

  3. xuyushun441-sys commented on Aug 3, 2026

    @xuyushun441-sys
    Contributor

    PM 验收:ACCEPT — PR #3246,已入合并队列(等剩余检查转绿自动落地)

    重派后一次成功。按 GitHub 实况核对:

    项 结果
    PR targets main、Fixes objectstack-ai/objectui#3227 ✅
    改动范围 ✅ 3 个文件(+122/−24),全部在 packages/layout + packages/types 内
    content/docs/releases/ 零改动 ✅
    CI 已完成的全绿、零失败;Type Check / Lint / 4 个 test shard / Bundle Analysis 仍在跑,已开 auto-merge 由队列在全绿后落地

    三处做得比我要求的更到位

    1. 四条 pin 一起搬,而不是只搬点名的那条。 我在派发里提醒了同文件第 95 行的 _DocumentIsNotNode 是同一契约的另一面;它不仅搬了这两条,还识别出 _PageIsReal 是承重的——Equal<any, X> 恒为 false、keyof any admits 一切,所以一个 any 的 SpecPage 会让两条主 pin 同时空转通过而什么都没检查。这条不搬过去,新住处的 pin 就是纸糊的。这是我没写进派发要求的。

    2. 回答了原单那句「若发现别处已有等价覆盖,核实后再决定」。 结论是没有等价覆盖,而且论证方式对:p1-spec-alignment.test.ts 里三处 const page: PageNodeSchema = { type: 'page', … } 是可赋值性检查,只能证明 'page' 被接受;把判别式放宽成 'page' | 'page-node' 之后它们全都还是绿的,只有 Equal<> 会红。所以 Equal 绝不能软化成 Extends —— 这一点被写进了新文件的文件头。

    3. 先证明目的地不空转,再搬。 这正是我引 objectstack#4642 时担心的:packages/types 的 tsconfig.json 排除测试文件(它们不能进 dist),但 tsconfig.test.json 又把它们收回来,并且 type-check 脚本是 tsc --noEmit && tsc -p tsconfig.examples.json && tsc -p tsconfig.test.json,CI 跑 pnpm type-check。链路完整,pin 是真的。这条依赖关系也写进了文件头:「若本包哪天不再跑 tsc -p tsconfig.test.json,下面这些就静默变成注释」。

    sabotage 验证是决定性的

    把判别式放宽成 'page' | 'page-node' 之后:

    src/__tests__/page-node-type-contract.test.ts(95,38): error TS2344: Type 'false' does not satisfy the constraint 'true'.
    

    全包只有这一条错误,就是被搬的那条 pin。 比改成 'page-node' 那次(会连带 p1-spec-alignment 报 3 条 TS2322)更能说明问题:放宽才是实践中真正会发生的漂移(生产者仍可合法发出注册表没有条目的键),而那种情况下只有这条 pin 拦得住。

    两处收尾也对

    • packages/layout 那份文件头改写成了对剩下内容成立的描述(batch 7 唯一幸存的 PageHeaderProps → PageHeaderComponentProps),并留了指向新住处的指针和一句 「不要在这里加回来,去那边扩展」;
    • 在 PageNodeSchema 声明本身加了 Tripwire: 反向引用(沿用同文件 PageNodeRegion 已有的写法),让守卫从被守的类型出发就能找到 —— 这正好治了原单说的病:下一个人看到守卫时能知道它为什么在。

    新文件从 barrel(../index)导入而非 ../layout,理由写明了:barrel 才是 @object-ui/components 等消费者看见的表面。

    changeset:同意不加

    两个测试文件 + 一处纯文档注释改动(PageNodeSchema 的结构一字未动),dist 对消费者逐字节相同。Changeset Fixed Group Check 判绿,与该判断一致。


    Generated by Claude Code

  4. xuyushun441-sys commented on Aug 3, 2026

    @xuyushun441-sys
    Contributor

    更正一条我引用的证据(结论不变)

    我在上面的验收里写过:

    changeset:同意不加。……Changeset Fixed Group Check 判绿,与该判断一致。

    这条引用应当撤回,与 #3207 上那条同因:#3212(PR #3253)查明 changeset-check 跑的是 scripts/check-changeset-fixed.mjs,校验的是 changeset fixed 组的成员资格,不检查 PR 有没有加 changeset —— 加不加都会绿,因此它对"该不该加"零信息量。

    不加 changeset 的真实依据不受影响:本 PR 是两个测试文件加一处纯文档注释改动(PageNodeSchema 结构一字未动),消费者编译依赖的 dist 逐字节相同。


    Generated by Claude Code

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

Metadata

Metadata

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions