Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 8 additions & 2 deletions .changeset/book-job-translation-app-authorwarn-keys-retired.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,14 @@ Run `os migrate meta --from 16` to rewrite existing sources automatically.
migration table steered retired `errors:` authors straight into it. **That
guidance entry is rewritten here**: retiring one dead key by pointing at
another is the defect, not the fix.
- **`app.homePageId`** — *its own hedge*. "If not set, usually defaults to the
first navigation item" described the only behaviour there was.
- **`app.homePageId`** — *a second source for one fact*. Not unread: objectui's
console consumed it in `resolveLandingRoute()` and it was the only thing
deciding where an app opened. (This entry first shipped saying otherwise;
corrected in #4709, which upheld the removal.) What condemns the key is its
shape — an ID cross-reference into `navigation` with no referential integrity,
falling back to the first item *silently* when the id dangled. If "land
somewhere other than first" is ever wanted again it belongs on the navigation
item itself, not on a pointer that can miss.
- **`app.areas[].order`** — *the sibling that works*. Nav-item `order` really is
sorted; area-level order never was, and both renderers iterate the array as
authored.
Expand Down
34 changes: 34 additions & 0 deletions .changeset/homepageid-tombstone-premise-corrected.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
---
'@objectstack/spec': patch
---

docs(spec): `app.homePageId` 的墓碑说清真正的退役理由 —— 「no shell ever read it」是假的 (#4709)

**改的是「为什么删」的表述,不是删本身。** `app.homePageId` 在 17.0.0 依旧退役
(`retiredKey`:编译期 `never`、解析期报错),conversion `app-dead-authoring-keys-removed`
的行为、baseline、`os migrate meta --from 16` 的处方一字未动。

#4667 给出的理由是「no shell ever read it」。这句是**假的**,而且与本仓自己的记录直接
矛盾 —— 2026-06 的 AppSchema liveness 审计
(`docs/audits/2026-06-appschema-property-liveness.md`)把 `homePageId` 明确列在 LIVE
一侧,因为 objectui console 的 `resolveLandingRoute()`
(`packages/app-shell/src/console/AppContent.tsx`,objectui @785b8a5d)一直在读它,而且
它是**唯一**决定「app 打开时落在哪」的地方。两份文档矛盾了两个月无人发现,直到有人做
cloud pin 对账时先信了这句、再去核渲染器才发现不对(#4709)。

真正让这个键该走的是它的**形状**,不是无人使用:它把落地页编码成指向 `navigation` 的
ID 交叉引用,没有引用完整性 —— id 悬空时**静默**回退到第一项(objectui 的实现正是如此),
于是同一件事有两个来源,而错的那个不出声。将来若要「落地页 ≠ 第一项」,正确形状是导航项
自身的标记(`navigation[].landing`:单一来源、不可能悬空),并按 enforce-first 设计
(先有渲染器与测试,再进 schema)。

墓碑文案改为诚实版本后,作者看到的处方**保持不变**:删掉这个键;要改 app 从哪里打开就
重排 `navigation` 让目标项排第一;根落地由 `isDefault` 决定。同步纠正:conversion 摘要
(经 `gen:upgrade-guide` / `gen:spec-changes` 重生成到 `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`
里那句「has no console consumer yet」,并给 6 月审计补了一条指向 #4667/#4709 的后续注记
(审计结论本身是对的,原文不动)。新增一条 pin 测试,防止「无人读过」这类假前提回潮。

objectui 侧那段永远进不去的 `if (homePageId)` 死分支单独清理:
`objectstack-ai/objectui#3264`。
2 changes: 1 addition & 1 deletion content/docs/references/ui/app.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ const result = ActionNavItemSchema.parse(data);
| **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 |
| **areas** | `{ id: string; label: string; icon?: string; description?: string; … }[]` | optional | Navigation areas for partitioning navigation by business domain |
| **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 |
| **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. |
| **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. |
| **requiredPermissions** | `string[]` | optional | Permissions required to access this app |
| **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. |
| **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. |
Expand Down
5 changes: 3 additions & 2 deletions content/docs/ui/apps.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -284,8 +284,9 @@ const projectApp = {

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

Expand Down
12 changes: 12 additions & 0 deletions docs/audits/2026-06-appschema-property-liveness.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,5 +18,17 @@ These are the highest-priority fixes (authoring them per spec fails; they only w
`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`.
- 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).

> **Later (2026-08-03, #4709) — `homePageId` is retired; this row's LIVE verdict was right.**
> #4667 removed `app.homePageId` in spec 17.0.0 justifying it with "no shell ever read it" — which
> **contradicted this audit** and was false: objectui's console read it in `resolveLandingRoute()`
> (`packages/app-shell/src/console/AppContent.tsx`, @785b8a5d), exactly as recorded above. #4709
> **upheld the removal on a different reason** and corrected the copy everywhere it appeared: the key
> encoded the landing page as an ID cross-reference into `navigation` with no referential integrity,
> silently falling back to the first item when it dangled. Post-v17 the landing page is the first
> navigation item; if the capability returns it belongs on the navigation item itself
> (`navigation[].landing`), enforce-first. Nothing above is amended — it is kept as written, because
> the failure it exposes is procedural: **a retirement citing liveness must reconcile against the
> existing audit record**, and for two months nobody noticed these two documents disagreed.

## Recommendation
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.
2 changes: 1 addition & 1 deletion docs/protocol-upgrade-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -199,7 +199,7 @@ The same pass closes `activationEvents` (#4657): both keys that carried it — `
| `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 |
| `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 |
| `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 |
| `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, #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) | retired — `migrate meta` only |
| `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, #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) | retired — `migrate meta` only |
| `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 |
| `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 |
| `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 |
Expand Down
4 changes: 2 additions & 2 deletions examples/app-showcase/src/ui/apps/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,8 @@ export const ShowcaseApp = App.create({
branding: { primaryColor: '#7C3AED' },

navigation: [
// First item = the app's landing surface (the delivered convention; the
// spec's homePageId has no console consumer yet).
// First item = the app's landing surface the only way to say it since
// `homePageId` was removed in spec 17.0.0 (#4667, #4709).
{ id: 'nav_capability_map', type: 'page', pageName: 'showcase_capability_map', label: 'Capability Map', icon: 'map' },
{ id: 'nav_start_here', type: 'page', pageName: 'showcase_start_here', label: 'Page Authoring', icon: 'compass' },
{
Expand Down
2 changes: 1 addition & 1 deletion packages/spec/liveness/app.json
Original file line number Diff line number Diff line change
Expand Up @@ -228,7 +228,7 @@
"homePageId": {
"status": "dead",
"verifiedAt": "2026-08-02",
"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."
"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."
},
"requiredPermissions": {
"status": "live",
Expand Down
4 changes: 2 additions & 2 deletions packages/spec/spec-changes.json
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,7 @@
},
{
"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",
"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)",
"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)",
"conversionId": "app-dead-authoring-keys-removed",
"toMajor": 17
},
Expand Down Expand Up @@ -758,7 +758,7 @@
},
{
"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",
"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)",
"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)",
"conversionId": "app-dead-authoring-keys-removed",
"toMajor": 17
},
Expand Down
Loading
Loading