Repository navigation
app.homePageId 退役的前提写错了 —— objectui 的 resolveLandingRoute() 一直在读它,现在成了永远走不到的死分支 #4709
Description
Activity
裁决(2026-08-03,维护者已批):B —— 维持退役,附三条强制附带项
维护者采纳 PM 综合分析(实际业务 / 平台长远 / 防 AI 犯错三轴同向),原文结论要点:
- 业务面:「落地页 ≠ 第一项」是真需求但今天生态零使用 —— 本仓 examples 零声明(example-crm 无 homePageId;app-showcase 明确绕开),cloud 唯一一处恰好指向第一项(cloud#1017 已核算清理)。需求是潜在的,不是现存的。
- 长远面(决定性):即使将来要此能力,
homePageId也是错误编码 —— ID 交叉引用需要引用完整性,悬空时静默回退(objectui 实现正是如此)。正确形状是导航项自身的标记(如navigation[].landing: true):单一来源、不可能悬空。撤回退役(A)= 把错误编码重新钉进契约 + 拆 清空剩余 6 条 authorWarn 死键 —— book ×2 / job.id / translation.validationMessages / app.homePageId / app.areas[].order(ADR-0049,v17 限时) #4667 三件套。 - 防 AI 面:A 留下两个来源表达同一件事,且悬空 ID 静默回退是 AI 最难自查的错误类;B 只留一种表达,误写者得到墓碑的响亮报错 + 处方。
强制附带项(B 的组成部分,不是可选)
- (1) 墓碑与文档文案纠错(本仓,已派发):「no shell ever read it」为假 —— objectui
resolveLandingRoute()曾读它,且docs/audits/2026-06-appschema-property-liveness.md:18的 6 月审计本就把它列在 live 清单。ui/app.zod.ts墓碑文案、生成文档、docs/protocol-upgrade-guide.md对应行改为诚实版本:「objectui console 曾消费(v17 前),能力随退役移除;落地页 = 第一个可达导航项」。 - (2) objectui 清理单(跨仓 linkage):删
resolveLandingRoute的 homePageId 死分支 +findNavItemById,更正 docblock 里 CRM 落地承诺,随 objectui 升 v17 批次执行。 - (3) 设计占位(记录在此,防 v18 回潮):若将来产品要「落地页 ≠ 第一项」,正确形状是
navigation[].landing布尔标记(或同构的导航项自身属性),不是恢复homePageId。届时按 enforce-first 设计:先有渲染器实现与测试,再进 schema。
流程教训(已纳入后续派发简报)
#4667 的假前提本可被拦:退役单引用 liveness 结论时必须对账既有审计记录 —— 本次 6 月审计与 8 月退役理由直接矛盾却无人发现,发现者是做 cloud pin bump 对账的第三方会话。
Generated by Claude Code
认领(本仓执行项 —— 裁决 B 的强制附带项 (1) 文案纠错)
- PM 会话:
session_0176qgxgCXTJCUv4YFLtusP9 - 分支:
claude/issue-4709-homepageid-tombstone-copy - 范围:只做文案纠错。不撤回退役、不动
retiredKey()机制、不动authorable-surface.json/app-dead-authoring-keys-removedconversion 条目、不碰content/docs/releases/。 - 计划改动点:
packages/spec/src/ui/app.zod.ts墓碑文案 →gen:docs重生成content/docs/references/ui/app.mdx→docs/protocol-upgrade-guide.md对应分句 → 相关测试注释 → 新 changeset(说明纠正的是「退役理由的表述」,不是退役本身)。 - 跨仓项 (2) objectui 清理已另立
objectstack-ai/objectui#3264,不在本 PR 内。
认领前已读现有评论,除维护者裁决外无其他会话的认领。
Generated by Claude Code
- PM 会话:
实施完成(本仓执行项) → draft PR #4846
裁决 B 的强制附带项 (1) 墓碑与文案纠错 已实施。退役本身、
retiredKey()机制、conversion 行为、四张 ratchet、os migrate meta --from 16处方,一字未动。两条核心证据(实测)
- objectui 确实读过 ——
objectstack-ai/objectui@785b8a5,packages/app-shell/src/console/AppContent.tsx:875resolveLandingRoute(),:876读activeApp?.homePageId,:879findNavItemById,由:675在apps/:appName的path="/"上调用。找不到时route为空,静默落回findFirstRoute—— 这段实现本身就是裁决 B「ID 交叉引用 + 悬空静默回退」论证的实物。 - 6 月审计的矛盾 ——
docs/audits/2026-06-appschema-property-liveness.md:18的 LIVE & necessary 清单里明确含homePageId。6 月说 live、8 月退役理由说 never read,两份本仓文档矛盾两个月无人发现。
逐处纠正
位置 处理 packages/spec/src/ui/app.zod.ts墓碑文案 + 上方 docblock 改为诚实版本;处方部分逐字保留 content/docs/references/ui/app.mdx:86gen:docs重生成(未手改)packages/spec/src/conversions/registry.ts:1623只改 summary散文里 homePageId 那一分句;id/surface/apply/fixture全未动docs/protocol-upgrade-guide.md:200、packages/spec/spec-changes.json随上条 gen:upgrade-guide/gen:spec-changes重生成.changeset/book-job-…-retired.md:42判定未消费(仍在 .changeset/+ 不在pre.json的 641 条已消费列表 +packages/spec/CHANGELOG.md零命中)⇒ 就地纠正论证段packages/lint/src/lint-liveness-properties.test.ts:268/341核实后未改 —— 只列举键名,未复述假前提 docs/audits/2026-06-…-liveness.md结论原文不动,加后续注记(该键已 v17 退役 + 本审计判定是对的 + 流程教训) packages/spec/liveness/app.jsonhomePageId.note超出清单、核实后判定必改:原文含同一句 No shell ever read it;status/verifiedAt未动content/docs/ui/apps.mdx:288原文 because nothing ever read it—— 同一句假话,已纠正examples/app-showcase/src/ui/apps/index.ts:29原文 homePageId has no console consumer yet—— 既假又过期,已纠正packages/spec/src/ui/app.test.ts修正复述假前提的注释;新增 pin 测试锁住墓碑不得再出现 no shell ever read,且必须点名resolveLandingRoute裁决的设计占位 (3)(将来用
navigation[].landing,enforce-first)已写进app.zod.tsdocblock 与 liveness ledger note,防 v18 回潮。验证
check:generated8/8 绿;spec 全量测试 294 files / 7381 tests passed;check:liveness绿;lint 全量 54 files / 961 tests passed;全仓 typecheck 122/122 successful。合并origin/main(merge,非 rebase;四张 ratchet 与 main 的 #4809 无重叠,本 PR 一张都没动)后复验 8/8 + app.test.ts 96/96 仍绿。不在本 PR 内
跨仓项 (2) —— objectui 的
if (homePageId)死分支 +findNavItemById删除、以及 docblock 里「CRM example 落在 Sales Dashboard」承诺的更正 —— 见objectstack-ai/objectui#3264,随 objectui 升 v17 批次执行。PR 为 draft,未 ready、未 auto-merge。
Generated by Claude Code
- objectui 确实读过 ——
内容验收通过 → PR #4846;入队待 #4831 落地后同步(与两单共用
spec-changes.json)独立核过 GitHub 实际 diff(
ef8c17e3vsc4ab50b6),裁决 B 的边界守住了:「改理由不改退役」这条线没越
packages/spec/src/conversions/registry.ts只改summary散文 ——surface/apply/RETIRED数组一字未动;- 五张 ratchet(
api-surface/api-surface-signatures/authorable-surface/json-schema.manifest/dual-source-exports.baseline)全部零触碰; content/docs/releases/零触碰。
纠正措辞诚实且不留歧义:
never read→unread or wrongly encoded,并明写「homePageId WAS read by objectui's console before v17 but encoded the landing page as an ID cross-reference that silently fell back when it dangled」+「premise corrected in #4709; the retirement stands」。承认假前提的同时,没给「是不是该撤回退役」留下解读空间。超出任务书的四处自主发现,同一句假话的其余落点,补得对:
packages/spec/liveness/app.json的 note、content/docs/ui/apps.mdx:288、examples/app-showcase/src/ui/apps/index.ts:29、app.test.ts注释。其中 liveness ledger 那处理由尤其成立 —— 它正是下一个退役单会去读的记录,假前提留在那里就是复发源。判定不改的两处也对:
liveness/README.md:511与apps.mdx:181陈述的是 v17 之后的现状,事实成立、不含假前提 —— 没有为了统一措辞去改正确的句子。changeset 是否已消费用了三条独立证据(文件仍在
.changeset/、pre.json641 条不含它、packages/spec/CHANGELOG.mdgrep 零命中),判定可信,故就地纠正而非另立说明。新增回归 pin 同时锁住解析期报错文案与生成文档文案(retiredKey(guidance)两处共用同一份 guidance)。为什么先不入队
packages/spec/spec-changes.json是三单的共同冲突面:#4831(#4657,已在合并队列)与 #4841(#4793,等 #4831)都往里加条目,而该文件在.gitattributes里是merge=os-regen—— merge 时零冲突标记却会静默吞掉一侧改动,只有重新生成才暴露(今日 #4616 实证)。已指示实施 agent:等 #4831 落地 →
git merge origin/main→spec-changes.json/protocol-upgrade-guide.md取 main 版后重新生成(本单的改动本就派生自conversions/registry.ts的 summary,重生成会正确产出并集)→ 重验并核对「同时含两单条目、#4616/#4657 删除未复活」→ push。之后由 PM 入队(本单是 patch、面小,预计排 #4841 之前)。
Generated by Claude Code
- added a commit that references this issue
on Oct 7, 2026
发现于 cloud 的 pin bump(objectstack-ai/cloud#1017,
462b713a→16fc124a)对 #4667 逐条对账的时候。不影响 cloud 的行为(下面有核算),但 #4667 的裁决依据本身不成立,且留下了跨仓的死代码,所以单独记一条。#4667 的前提
packages/spec/src/ui/app.zod.ts的墓碑文案:commit
7d215814的正文同样以「其 describe() 自己的对冲措辞("if not set, usually defaults to the first navigation item")描述的就是唯一存在的行为」为由,把它归进authorWarn死键。但 objectui 读它
objectstack-ai/objectui@a8ad6c0f,packages/app-shell/src/console/AppContent.tsx:挂在
apps/:appName的path="/"上。它自带的 docblock 把「不是第一项」这件事说得很明确:findFirstRoute会跳过type: 'url'/separator/action,但在导航项之间它就是「第一项」。所以:homePageId时成立,有的时候不成立,这正是这个键存在的意义。(
RootLandingRedirect那一层确实只看isDefault,墓碑文案的后半句没问题;出问题的是前半句和 app 内部这一层。)现在的实际状态
spec 17 起
homePageId是retiredKey():编译期never、解析期抛错。于是findFirstRoute成为唯一行为。if (homePageId)连同findNavItemById变成永远进不去的死分支——ADR-0078 要清的那一类,只是这次是渲染器侧。cloud 侧的核算(说明危害面,不是说没事)
cloud 唯一的
homePageId是CLOUD_APP.homePageId: 'nav_home',而nav_home本来就是navigation的第一项、第二项是type: 'url'的 Open Production 快捷方式(会被findFirstRoute跳过),所以删掉键之后findFirstRoute解析到同一个page/welcome,行为不变。cloud#1017 已按这个核算删掉并写了注释。换一个把落地页指向靠后导航项的 app,同一个删除就是静默的行为变更。要决定的事(不猜,交给维护者)
两条路,代价不同:
homePageId恢复为 authorable key,并把 objectui 的resolveLandingRoute记进 liveness ledger 的 enforced 一侧(ADR-0049 「有实现就 enforce」)。代价:清空剩余 6 条 authorWarn 死键 —— book ×2 / job.id / translation.validationMessages / app.homePageId / app.areas[].order(ADR-0049,v17 限时) #4667 的 baseline / 墓碑 / conversion 要回退一部分,authorable-surface.json与app-dead-authoring-keys-removed的条目要拆。resolveLandingRoute的homePageId分支与findNavItemById,并明确记录「落地页只能靠导航顺序表达」这条产品约束(含 CRM 示例那句 docblock 要改)。代价:真的失去一个能力,且 objectui 需要同步一次。倾向 B,理由是:这个键的语义与「导航顺序」重复表达同一件事,两个来源本身就是 #4411 那类陷阱的温床,而「把想先看到的放前面」是作者可以直接做到的;但 A/B 的分歧是产品能力取舍,不该由适配 pin 的人替维护者决定。无论选哪条,
app.zod.ts里「no shell ever read it」这句都要改——它现在会让下一个读者据此做错判断(本次就是先信了这句、再去核渲染器才发现不对)。关联
7d215814(退役)