Repository navigation
[移交自 objectui · v17 阻塞] app.homePageId 退役墓碑写着「no shell ever read it」,但 objectui 的 AppContent 正在读它 #4865
Description
Part of objectstack-ai/objectui#3287
按 /pm-dispatch 跨分片移交协议从 objectui 分片转来:共享契约面(packages/spec 的退役裁决)单一 owner 是主 backlog PM,objectui 分片 PM 不自行处置。
- 移交方:objectui 分片 PM,会话
session_01NVPjPzmmAJ2Ngtvgg5MSRa(账号xuyushun441-sys,已在 [PM] 分片分工登记表 — 谁在管哪个仓库的队列 #4604 登记) - 源单:spec 17.0.0 正式版会退役
app.homePageId,但 objectui 的AppContent正在读它 —— 升级前需要拍板 objectui#3287(已标needs-user-decision,在本单有结论前 objectui 侧不动任何一处) ⚠️ 这是 objectui 升级到@objectstack/spec17.0.0 正式版的前置阻塞项
结论先行:#4667 的墓碑有一处事实性错误
packages/spec 在 #4667 / #4680(ADR-0049 enforce-or-remove)把 app.homePageId 退役为 retiredKey() 墓碑,文案是:
app.homePageIdwas removed in @objectstack/spec 17.0.0 (#4667, ADR-0049) — no shell ever read it. An app's landing page IS its first navigation item (byorder), and the root landing followsisDefaultrouting.
「no shell ever read it」不成立。 objectui —— 官方渲染器 —— 一直在读它:
packages/app-shell/src/console/AppContent.tsx:870-885,resolveLandingRoute():
function resolveLandingRoute(activeApp: any, ctx?: NavTemplateContext): string {
const homePageId: string | undefined = activeApp?.homePageId;
const navigation = activeApp?.navigation || [];
if (homePageId) {
const item = findNavItemById(navigation, homePageId);
const route = buildItemRoute(item, ctx);
if (route) return route;
}
return findFirstRoute(navigation, ctx);
}而且该函数的注释专门解释了它为什么存在:
Honors the app's explicit
homePageId(Salesforce-style "Default Landing"); falls back to the first reachable nav item only when no homePageId is set or it points at something that doesn't yield a route. This is what lets the CRM example open on the Sales Dashboard instead of the Lead list.
为什么这不只是文案问题
墓碑的替代方案(「落地页就是 navigation 的第一项,按 order」)与被退役的键语义不同,不是同义改写:
| 退役前 | 退役后 | |
|---|---|---|
显式钉了 homePageId 的 app |
落在被指定的那一项 | 落在 nav 第一项 |
也就是说,退役会让所有显式设置过落地页的 app 静默改变落地位置。CRM 示例原本落在 Sales Dashboard,退役后落在 Lead 列表。这不是"删掉一个没人用的键",是"移除一个有真实消费者、且行为可观测的能力"。
大概率是跨仓 liveness 审计的盲区
homePageId 的消费者不在 objectstack 仓内,而在 objectui 的 shell 里。仓内 grep 会得出「没有消费者」,这与我们在 objectui#3226 上刚遇到的形态是同一族:
当一个键的消费者在仓外时,仓内 grep 不构成「没有消费者」的证据。
建议核查 #4667 的 liveness 判定是如何得出「no shell ever read it」的 —— 如果它只扫了 objectstack 仓,那么同一批退役里的其它键也可能有同样的盲区,这比单个键的去留更值得先查一遍。
需要拍板的
A. 裁决正确,objectui 跟随删除。 落地语义统一为「nav 第一项 + 根落地看 isDefault」,作者改落地页靠重排 navigation。需要 os migrate meta --from 16 覆盖这次静默行为变化,并且墓碑文案应改掉「no shell ever read it」这句(它是错的,会误导后来者)。
B. 前提有误,该键不该退役(或改为标准弃用周期而非直接移除)。若「显式指定落地页」是想要的能力,objectui 已经实现了它。
我的倾向是先确认事实再定去留 —— 无论最终选 A 还是 B,墓碑上那句断言都需要修正,因为它会让后来者以为这次移除没有消费者。
objectui 侧的影响面(A 路线下,7 处,届时一并改)
packages/app-shell/src/console/AppContent.tsx—resolveLandingRoute()的homePageId分支packages/types/src/app.ts:415—homePageId?: stringpackages/types/src/__tests__/page-app-dashboard-spec-parity.test.ts:139— 期望键清单apps/console/src/preview-samples.ts:86— 样例(objectui#3266 刚按当时的 spec 加上)apps/console/src/__tests__/preview-samples-spec-valid.test.ts:162— 其提示文案指向homePageId,届时会指向一个同样已退役的键- objectui#3275 / PR fix(cli): dispatch OS_DATABASE_DRIVER=memory to the mingo InMemoryDriver (#3276) #3285 中
AppPreview新增的homePageId读取 apps/console/src/components/RootLandingRedirect.tsx的注释
版本现状(实测)
- objectui pin
^17.0.0-rc.1,lockfile 解析到17.0.0-rc.1,也是 npm 上最新已发布版本; - 该已发布构建里
homePageId仍是合法可选键(dist/app.zod-*.d.ts:764;ObjectStackSchema.safeParse()带该键 PASS); - 退役只存在于 objectstack
main,尚未发布。
所以今天 objectui 读它是正确的,问题只在升级那一刻爆发。
关联:objectui#3287、objectui#3275 / PR #3285、objectui#3266、#4001(移除 landing,当时的替代正是 homePageId)、#4667 / #4680、ADR-0049
Activity
xuyushun441-sys commentedon Aug 3, 2026
维护者裁决转录(2026-08-03,objectui 侧 PM 会话)
维护者认可 B 先行——主张撤销
app.homePageId的退役:AppContent.resolveLandingRoute()在读,注释写明存在理由);并追加节奏要求:协议变更集中在 v17 大版本一次了结——本单需在 spec 17.0.0 正式版发布之前裁决(npm 最新仍是
17.0.0-rc.1,窗口还在):同时重申本单原建议:优先核查 #4667 的 liveness 审计是否只扫了本仓——同批退役键可能共享跨仓盲区,单个键的去留是小事,审计方法有洞是大事。
Generated by Claude Code
Generated by Claude Code
xuyushun441-sys commentedon Aug 3, 2026
补充:liveness 判定跨仓盲区的第二个数据点(方向相反)
objectui#3172 的只读审计(2026-08-03)发现:spec liveness 账本
packages/spec/liveness/flow.json把/props/nodes/children/position判 live,note 是 "designer canvas layout (no engine consumer)"——但 designer(objectui 的FlowCanvas)读写的是本地拼写ui:{x,y},两个仓在审计时点都没有任何FlowNode.position的读者。这是一条假 live(依据了一个不存在的消费者),与app.homePageId的假 dead(漏掉一个存在的消费者)互为镜像——同一个跨仓证据盲区,两个方向都会出错。该键本身无需 objectstack 动作:objectui 正在把设计器收敛到
position(v17 窗口,objectui#3172 已派实施),落地后账本这条自然变真。补充此数据点只为强化本单原建议:核查 #4667 那套 liveness 方法的跨仓覆盖,两个方向都要。Generated by Claude Code
Generated by Claude Code
xuyushun441-sys commentedon Aug 3, 2026
app.homePageId 确定退役
xuyushun441-sys commentedon Aug 3, 2026
维护者终裁(2026-08-03)——
app.homePageId确定退役墓碑的事实断言虽错(objectui 确有真实读者,见 objectui#3287),但产品裁决维持退役:落地语义全平台统一为「nav 首项(按
order)+isDefault根落地」,不保留显式落地页能力。对 rc.2 切版的影响:零——objectstack main 现状(含退役)即为正确状态,无需任何动作。
objectui 侧执行路径(objectui#3287 已激活路径 A):7 处消费点随
@objectstack/spec@17.0.0-rc.2的 pin-bump PR 一并删除 + 迁移说明;存量 app 的落地页语义变更由os migrate meta --from 16覆盖。方法学部分拆出为 #4895(#4667 批次 liveness 证据路径的跨仓复核 + 墓碑文案更正)——裁决关闭的是这个键的争议,不是那个审计盲区。
本单问题已裁决,关闭。
Generated by Claude Code
Generated by Claude Code
docs(spec): HookContext.input 契约表改成引擎真正构造的形状 (objectstack-ai#5273) (o…