Skip to content

Commit e492b1c

Browse files
committed
docs(spec): app.homePageId 墓碑说清真正的退役理由 —— 「no shell ever read it」是假的 (#4709)
改的是「为什么删」的表述,不是删本身。`homePageId` 依旧退役(`retiredKey`:编译期 `never`、解析期报错),conversion `app-dead-authoring-keys-removed` 的行为、四张 baseline、`os migrate meta --from 16` 的处方一字未动。 #4667 的理由「no shell ever read it」是假的,且与本仓自己的记录矛盾:2026-06 的 AppSchema liveness 审计把 `homePageId` 列在 LIVE 一侧,因为 objectui console 的 `resolveLandingRoute()`(`packages/app-shell/src/console/AppContent.tsx`, objectui @785b8a5d)一直在读它,而且它是唯一决定 app 落地页的地方。两份文档矛盾 两个月无人发现 —— 假前提会让下一个读者据此做错判断,#4709 立单人正是先信了这句。 真正让它该走的是形状:把落地页编码成指向 `navigation` 的 ID 交叉引用,没有引用 完整性,id 悬空时静默回退到第一项。将来若要「落地页 ≠ 第一项」,正确形状是导航项 自身的标记(`navigation[].landing`),按 enforce-first 设计。 逐处纠正:`ui/app.zod.ts` 墓碑与 docblock、conversion 摘要(经 gen 重生成到 `docs/protocol-upgrade-guide.md` 与 `spec-changes.json`)、生成文档 `content/docs/references/ui/app.mdx`、`content/docs/ui/apps.mdx`、liveness ledger 的 `homePageId` note、`examples/app-showcase` 的过期注释、#4667 未消费 changeset 的论证段;6 月审计加后续注记(原结论不动)。新增 pin 测试防假前提回潮。 objectui 侧的 `if (homePageId)` 死分支另见 objectstack-ai/objectui#3264Fixes #4709 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0176qgxgCXTJCUv4YFLtusP9
1 parent 127f091 commit e492b1c

12 files changed

Lines changed: 120 additions & 25 deletions

File tree

.changeset/book-job-translation-app-authorwarn-keys-retired.md

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,14 @@ Run `os migrate meta --from 16` to rewrite existing sources automatically.
3939
migration table steered retired `errors:` authors straight into it. **That
4040
guidance entry is rewritten here**: retiring one dead key by pointing at
4141
another is the defect, not the fix.
42-
- **`app.homePageId`***its own hedge*. "If not set, usually defaults to the
43-
first navigation item" described the only behaviour there was.
42+
- **`app.homePageId`***a second source for one fact*. Not unread: objectui's
43+
console consumed it in `resolveLandingRoute()` and it was the only thing
44+
deciding where an app opened. (This entry first shipped saying otherwise;
45+
corrected in #4709, which upheld the removal.) What condemns the key is its
46+
shape — an ID cross-reference into `navigation` with no referential integrity,
47+
falling back to the first item *silently* when the id dangled. If "land
48+
somewhere other than first" is ever wanted again it belongs on the navigation
49+
item itself, not on a pointer that can miss.
4450
- **`app.areas[].order`***the sibling that works*. Nav-item `order` really is
4551
sorted; area-level order never was, and both renderers iterate the array as
4652
authored.
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
---
2+
'@objectstack/spec': patch
3+
---
4+
5+
docs(spec): `app.homePageId` 的墓碑说清真正的退役理由 —— 「no shell ever read it」是假的 (#4709)
6+
7+
**改的是「为什么删」的表述,不是删本身。** `app.homePageId` 在 17.0.0 依旧退役
8+
(`retiredKey`:编译期 `never`、解析期报错),conversion `app-dead-authoring-keys-removed`
9+
的行为、baseline、`os migrate meta --from 16` 的处方一字未动。
10+
11+
#4667 给出的理由是「no shell ever read it」。这句是**假的**,而且与本仓自己的记录直接
12+
矛盾 —— 2026-06 的 AppSchema liveness 审计
13+
(`docs/audits/2026-06-appschema-property-liveness.md`)把 `homePageId` 明确列在 LIVE
14+
一侧,因为 objectui console 的 `resolveLandingRoute()`
15+
(`packages/app-shell/src/console/AppContent.tsx`,objectui @785b8a5d)一直在读它,而且
16+
它是**唯一**决定「app 打开时落在哪」的地方。两份文档矛盾了两个月无人发现,直到有人做
17+
cloud pin 对账时先信了这句、再去核渲染器才发现不对(#4709)。
18+
19+
真正让这个键该走的是它的**形状**,不是无人使用:它把落地页编码成指向 `navigation`
20+
ID 交叉引用,没有引用完整性 —— id 悬空时**静默**回退到第一项(objectui 的实现正是如此),
21+
于是同一件事有两个来源,而错的那个不出声。将来若要「落地页 ≠ 第一项」,正确形状是导航项
22+
自身的标记(`navigation[].landing`:单一来源、不可能悬空),并按 enforce-first 设计
23+
(先有渲染器与测试,再进 schema)。
24+
25+
墓碑文案改为诚实版本后,作者看到的处方**保持不变**:删掉这个键;要改 app 从哪里打开就
26+
重排 `navigation` 让目标项排第一;根落地由 `isDefault` 决定。同步纠正:conversion 摘要
27+
(经 `gen:upgrade-guide` / `gen:spec-changes` 重生成到 `docs/protocol-upgrade-guide.md`
28+
`spec-changes.json`)、生成文档 `content/docs/references/ui/app.mdx`
29+
`content/docs/ui/apps.mdx`、liveness ledger 的 `homePageId` note、`examples/app-showcase`
30+
里那句「has no console consumer yet」,并给 6 月审计补了一条指向 #4667/#4709 的后续注记
31+
(审计结论本身是对的,原文不动)。新增一条 pin 测试,防止「无人读过」这类假前提回潮。
32+
33+
objectui 侧那段永远进不去的 `if (homePageId)` 死分支单独清理:
34+
`objectstack-ai/objectui#3264`

content/docs/references/ui/app.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ const result = ActionNavItemSchema.parse(data);
8383
| **navigation** | `{ id: string; label: string; icon?: string; order?: number; … } \| { id: string; label: string; icon?: string; order?: number; … } \| { id: string; label: string; icon?: string; order?: number; … } \| { id: string; label: string; icon?: string; order?: number; … } \| { id: string; label: string; icon?: string; order?: number; … } \| { id: string; label: string; icon?: string; order?: number; … } \| { id: string; label: string; icon?: string; order?: number; … } \| { type: 'separator'; id?: string; order?: number } \| { id: string; label: string; icon?: string; order?: number; … }[]` | optional | Full navigation tree for the app sidebar |
8484
| **areas** | `{ id: string; label: string; icon?: string; description?: string; … }[]` | optional | Navigation areas for partitioning navigation by business domain |
8585
| **contextSelectors** | `{ id: string; label: string; icon?: string; optionsSource: object; … }[]` | optional | App-level scope dropdowns whose value is injected into nav items as `{<id>}` template vars |
86-
| **homePageId** | `any` | optional | [REMOVED] `app.homePageId` was 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 (by `order`), and the root landing follows `isDefault` routing. Delete the key; to change where an app opens, reorder `navigation` so the intended entry is first, and set `isDefault` on the app that should own the root landing. Run `os migrate meta --from 16` to rewrite existing sources automatically. |
86+
| **homePageId** | `any` | optional | [REMOVED] `app.homePageId` was removed in @objectstack/spec 17.0.0 (#4667, #4709, ADR-0049). objectui's console did read it before v17 (`resolveLandingRoute`), so this key had a consumer — it was retired because the capability is better expressed on the navigation item itself than as an ID cross-reference that silently falls back when it dangles. An app's landing page IS its first navigation item (by `order`), and the root landing follows `isDefault` routing. Delete the key; to change where an app opens, reorder `navigation` so the intended entry is first, and set `isDefault` on the app that should own the root landing. Run `os migrate meta --from 16` to rewrite existing sources automatically. |
8787
| **requiredPermissions** | `string[]` | optional | Permissions required to access this app |
8888
| **objects** | `any` | optional | [REMOVED] `App.objects` was removed in @objectstack/spec 17.0.0 (2026-06 liveness audit — never read; the spec itself labelled it "config file convenience"). Objects belong to the stack (`defineStack({ objects })`); an app reaches them through its navigation items. Delete the key. |
8989
| **apis** | `any` | optional | [REMOVED] `App.apis` was removed in @objectstack/spec 17.0.0 (2026-06 liveness audit — never read). Declarative endpoints belong to the stack (`defineStack({ apis })`), not the app shell. Delete the key. |

content/docs/ui/apps.mdx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -284,8 +284,9 @@ const projectApp = {
284284

285285
requiredPermissions: ['pm_access'],
286286
// No `homePageId`: the landing page IS the first navigation item (by `order`),
287-
// and the ROOT landing follows `isDefault`. The key was removed in 17.0.0 (#4667)
288-
// because nothing ever read it.
287+
// and the ROOT landing follows `isDefault`. The key was removed in 17.0.0
288+
// (#4667, #4709) — it did have a consumer, but it pointed at a navigation item
289+
// by id and fell back silently when that id dangled.
289290
};
290291
```
291292

docs/audits/2026-06-appschema-property-liveness.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,5 +18,17 @@ These are the highest-priority fixes (authoring them per spec fails; they only w
1818
`name`, `label`, `description`, `icon`, `active`, `isDefault`, `hidden`, `navigation` (whole tree), `areas` (precedence over navigation), `contextSelectors` (all sub-fields), `homePageId`, `defaultAgent` (dual consumer: framework `agent-runtime.ts:341` + objectui chatbot), `branding.{primaryColor,favicon}`, `protection`. Nav-item union fully live: `id/label/icon/order/badge/visible(CEL)/requiredPermissions/requiresObject/requiresService` + per-type payloads (`objectName/viewName/recordId/recordMode/dashboardName/pageName/url/target/reportName/componentRef/params/children/expanded`). NavigationContribution (ADR-0029) live via `objectql/engine.ts:912`.
1919
- PARTIAL: `App.requiredPermissions` (app-entry gate not observed; only nav-item perms enforced), nav `type:'action'` `actionDef` (fires `onAction(item)`; `actionDef.{actionName,params}` shape read loosely in the action runtime, 0 direct grep in shell).
2020

21+
> **Later (2026-08-03, #4709) — `homePageId` is retired; this row's LIVE verdict was right.**
22+
> #4667 removed `app.homePageId` in spec 17.0.0 justifying it with "no shell ever read it" — which
23+
> **contradicted this audit** and was false: objectui's console read it in `resolveLandingRoute()`
24+
> (`packages/app-shell/src/console/AppContent.tsx`, @785b8a5d), exactly as recorded above. #4709
25+
> **upheld the removal on a different reason** and corrected the copy everywhere it appeared: the key
26+
> encoded the landing page as an ID cross-reference into `navigation` with no referential integrity,
27+
> silently falling back to the first item when it dangled. Post-v17 the landing page is the first
28+
> navigation item; if the capability returns it belongs on the navigation item itself
29+
> (`navigation[].landing`), enforce-first. Nothing above is amended — it is kept as written, because
30+
> the failure it exposes is procedural: **a retirement citing liveness must reconcile against the
31+
> existing audit record**, and for two months nobody noticed these two documents disagreed.
32+
2133
## Recommendation
2234
Add the 3 drift fields to the spec (`accentColor`, `badgeVariant`, `separator`) **or** stop the renderer reading them. Prune the aspirational block; `App.sharing`/`App.embed`/`apiEnabled`-style props create a false security/feature impression.

docs/protocol-upgrade-guide.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -197,7 +197,7 @@ The same enforce-or-remove pass retires the `RestServerConfig.openApi31` block (
197197
| `flow-node-script-config-aliases` | `flow.node.script.config` | script flow-node config keys 'functionName' → 'function', 'input' → 'inputs' (#3796) | live — protocol 17 loader accepts the old shape |
198198
| `permission-rls-priority-removed` | `permission.rowLevelSecurity.priority` | RLS-policy key 'priority' removed (#3896 audit — policies OR-combine, so the promised conflict-resolution semantics cannot exist; dropping it changes no outcome) | retired — `migrate meta` only |
199199
| `tool-inert-authoring-keys-removed` | `tool.category / tool.permissions / tool.active / tool.builtIn` | tool keys 'category'/'permissions'/'active'/'builtIn' removed (#3896 close-out — authorable and inert; permissions gated nothing, active:false withdrew nothing) | retired — `migrate meta` only |
200-
| `app-dead-authoring-keys-removed` | `app.version / app.aria / app.objects / app.apis / app.sharing / app.embed / app.mobileNavigation / app.contextSelectors.includeAll / app.contextSelectors.placement / app.homePageId / app.areas.order` | app keys 'version'/'aria'/'objects'/'apis'/'sharing'/'embed'/'mobileNavigation'/'homePageId' plus contextSelectors 'includeAll'/'placement' and areas 'order' removed (liveness audits #4001, #4509, #4667never read; sharing/embed declared a public surface no route enforced, mobileNavigation was fully unimplemented, includeAll was deliberately disobeyed because an 'All' row would clear a mandatory scope, the landing page IS the first nav item, and no renderer ever sorted areas) | retired — `migrate meta` only |
200+
| `app-dead-authoring-keys-removed` | `app.version / app.aria / app.objects / app.apis / app.sharing / app.embed / app.mobileNavigation / app.contextSelectors.includeAll / app.contextSelectors.placement / app.homePageId / app.areas.order` | app keys 'version'/'aria'/'objects'/'apis'/'sharing'/'embed'/'mobileNavigation'/'homePageId' plus contextSelectors 'includeAll'/'placement' and areas 'order' removed (liveness audits #4001, #4509, #4667unread or wrongly encoded; sharing/embed declared a public surface no route enforced, mobileNavigation was fully unimplemented, includeAll was deliberately disobeyed because an 'All' row would clear a mandatory scope, 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 — the landing page is the first nav item (premise corrected in #4709; the retirement stands), and no renderer ever sorted areas) | retired — `migrate meta` only |
201201
| `app-area-fail-open-gates-removed` | `app.areas.visible / app.areas.requiredPermissions` | navigation-area keys 'visible'/'requiredPermissions' removed (#4651, ADR-0049 — FAIL-OPEN access gates: no layer ever read them, so a 'hidden' or permission-gated area was served and rendered to every user, while the identically named keys on a navigation ITEM and on the APP are enforced; gate the items inside the area, or gate the app) | retired — `migrate meta` only |
202202
| `field-required-notnull-explicit` | `object.fields.*.required / object.fields.*.storage.notNull` | required fields gain explicit 'storage.notNull: true' (ADR-0113 — pre-17 'required' implied the column constraint; post-17 it is only the write contract) | retired — `migrate meta` only |
203203
| `action-inert-keys-removed` | `action.shortcut / action.bulkEnabled` | action keys 'shortcut'/'bulkEnabled' removed (#3896 close-out — no keydown path dispatches shortcuts; the multi-select toolbar reads the view's bulkActions) | retired — `migrate meta` only |

examples/app-showcase/src/ui/apps/index.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,8 @@ export const ShowcaseApp = App.create({
2525
branding: { primaryColor: '#7C3AED' },
2626

2727
navigation: [
28-
// First item = the app's landing surface (the delivered convention; the
29-
// spec's homePageId has no console consumer yet).
28+
// First item = the app's landing surface the only way to say it since
29+
// `homePageId` was removed in spec 17.0.0 (#4667, #4709).
3030
{ id: 'nav_capability_map', type: 'page', pageName: 'showcase_capability_map', label: 'Capability Map', icon: 'map' },
3131
{ id: 'nav_start_here', type: 'page', pageName: 'showcase_start_here', label: 'Page Authoring', icon: 'compass' },
3232
{

packages/spec/liveness/app.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -228,7 +228,7 @@
228228
"homePageId": {
229229
"status": "dead",
230230
"verifiedAt": "2026-08-02",
231-
"note": "REMOVED 2026-08-02 (#4667) — tombstoned at the schema (retiredKey carries the prescription; authoring it is a tsc error and a parse error) and stripped from sources by the protocol-17 conversion. The entry stays because retiredKey keeps the key in the walked shape (the rls.priority precedent). No shell ever read it — the landing IS the first navigation item (by order), and the ROOT landing follows isDefault routing (objectui RootLandingRedirect). The schema own hedge (\"if not set, usually defaults to the first navigation item\") described the only behavior that existed. Reorder navigation, or set isDefault, instead. The three aliases that routed here (home / homepage / landingpage) were removed from AppSchema aliases and now carry the same prescription as guidance."
231+
"note": "REMOVED 2026-08-02 (#4667) — tombstoned at the schema (retiredKey carries the prescription; authoring it is a tsc error and a parse error) and stripped from sources by the protocol-17 conversion. The entry stays because retiredKey keeps the key in the walked shape (the rls.priority precedent). PREMISE CORRECTED 2026-08-03 (#4709), removal upheld: #4667 justified this row with \"no shell ever read it\", which was FALSE and contradicted this repo's own 2026-06 AppSchema liveness audit (docs/audits/2026-06-appschema-property-liveness.md), which listed homePageId LIVE. objectui's console DID read it — resolveLandingRoute(), packages/app-shell/src/console/AppContent.tsx @785b8a5d — and it was the only thing deciding where an app opened. The key is retired for its SHAPE: an ID cross-reference into navigation with no referential integrity, silently falling back to the first item when it dangled. Post-v17 the landing IS the first navigation item (by order), and the ROOT landing follows isDefault routing (objectui RootLandingRedirect). Reorder navigation, or set isDefault, instead; if the capability returns it belongs on the navigation item itself (navigation[].landing), enforce-first. The three aliases that routed here (home / homepage / landingpage) were removed from AppSchema aliases and now carry the same prescription as guidance. Process lesson: a retirement citing liveness MUST reconcile against existing audit records — this contradiction went unnoticed for two months."
232232
},
233233
"requiredPermissions": {
234234
"status": "live",

packages/spec/spec-changes.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -148,7 +148,7 @@
148148
},
149149
{
150150
"surface": "app.version / app.aria / app.objects / app.apis / app.sharing / app.embed / app.mobileNavigation / app.contextSelectors.includeAll / app.contextSelectors.placement / app.homePageId / app.areas.order",
151-
"to": "app keys 'version'/'aria'/'objects'/'apis'/'sharing'/'embed'/'mobileNavigation'/'homePageId' plus contextSelectors 'includeAll'/'placement' and areas 'order' removed (liveness audits #4001, #4509, #4667 — never read; sharing/embed declared a public surface no route enforced, mobileNavigation was fully unimplemented, includeAll was deliberately disobeyed because an 'All' row would clear a mandatory scope, the landing page IS the first nav item, and no renderer ever sorted areas)",
151+
"to": "app keys 'version'/'aria'/'objects'/'apis'/'sharing'/'embed'/'mobileNavigation'/'homePageId' plus contextSelectors 'includeAll'/'placement' and areas 'order' removed (liveness audits #4001, #4509, #4667 — unread or wrongly encoded; sharing/embed declared a public surface no route enforced, mobileNavigation was fully unimplemented, includeAll was deliberately disobeyed because an 'All' row would clear a mandatory scope, 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 — the landing page is the first nav item (premise corrected in #4709; the retirement stands), and no renderer ever sorted areas)",
152152
"conversionId": "app-dead-authoring-keys-removed",
153153
"toMajor": 17
154154
},
@@ -751,7 +751,7 @@
751751
},
752752
{
753753
"surface": "app.version / app.aria / app.objects / app.apis / app.sharing / app.embed / app.mobileNavigation / app.contextSelectors.includeAll / app.contextSelectors.placement / app.homePageId / app.areas.order",
754-
"to": "app keys 'version'/'aria'/'objects'/'apis'/'sharing'/'embed'/'mobileNavigation'/'homePageId' plus contextSelectors 'includeAll'/'placement' and areas 'order' removed (liveness audits #4001, #4509, #4667 — never read; sharing/embed declared a public surface no route enforced, mobileNavigation was fully unimplemented, includeAll was deliberately disobeyed because an 'All' row would clear a mandatory scope, the landing page IS the first nav item, and no renderer ever sorted areas)",
754+
"to": "app keys 'version'/'aria'/'objects'/'apis'/'sharing'/'embed'/'mobileNavigation'/'homePageId' plus contextSelectors 'includeAll'/'placement' and areas 'order' removed (liveness audits #4001, #4509, #4667 — unread or wrongly encoded; sharing/embed declared a public surface no route enforced, mobileNavigation was fully unimplemented, includeAll was deliberately disobeyed because an 'All' row would clear a mandatory scope, 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 — the landing page is the first nav item (premise corrected in #4709; the retirement stands), and no renderer ever sorted areas)",
755755
"conversionId": "app-dead-authoring-keys-removed",
756756
"toMajor": 17
757757
},

0 commit comments

Comments
 (0)