Skip to content

Commit d49fbc8

Browse files
committed
feat(spec)!: retire the six remaining authorWarn dead keys (#4667)
The #4488 liveness audit flagged as `authorWarn` the keys whose DECLARATION actively misleads — not merely unread, but shaped so an author reasonably concludes they configure something. #4509 and #4583 cleared the rest; these six are what remained, and each read alive for its own reason: book.translations proximity — `doc.translations`, two files book.groups[].translations over, same name and shape, works on every read path. These were parsed, stored and round-tripped, and rendered in the authoring locale to every reader. job.id its own describe(): "defaults to `name` when omitted" advertised an identity override that does not exist. `name` is the scheduling key, the sys_job row key and the JobExecution.jobId stamp — two jobs differing only in `id` were one job declared twice. translation.validationMessages the platform's own signposts, twice: the schema example showed a concrete override, and #3778's migration table steered retired `errors:` authors straight into it. app.homePageId its own hedge: "if not set, usually defaults to the first navigation item" described the only behaviour there was. app.areas[].order the sibling that works — nav-item `order` IS sorted; area order never was, and both renderers iterate the array as authored. Routes differ deliberately, and so does the ledger discipline that follows from them. `book.groups[].translations` and `app.homePageId` are TOMBSTONED (`retiredKey`: `never` at compile time, the prescription at parse time) and keep their ledger rows, because the key stays in the walked shape. The group schema is the reason: it is a plain `z.object` with no `.strict()`, where a bare delete would have zod silently strip the key — trading one silent no-op for another. The other four are strict deletions carrying `guidance`, and their rows are deleted. Retired alias spellings (`i18n`, `home`, `homepage`, `landingpage`, `sort`) route to the same prescriptions rather than renaming onto keys that are themselves gone. #3778's `errors` guidance is rewritten here rather than left alone: it had been retiring one dead key by pointing authors at another, so taking its advice moved content from one unread group to a second one. Third instance of this shape after `READ_ONLY_BELONGS_ON_DATASOURCE` (#4583) and `mapping.guidance.skipErrors` (#4509) — grep for prescriptions naming a key before you retire it. ADR-0087: three new conversions (`book-translations-removed`, `job-id-removed`, `translation-validation-messages-removed`) plus an extension of `app-dead-authoring-keys-removed` to drill the `areas` array; all wired into the protocol-17 D3 chain step. Two authoring sites the gates found that a grep would not have: the published `objectstack-i18n` skill taught `validationMessages` in a copy-paste example (an AI reproduces that verbatim — caught by tsc via check:skill-examples), and `examples/app-todo` authored the group in three locales, where the `en` entries merely duplicated the rule's own text and the zh-CN / ja-JP translations had never once been rendered. Care taken in the test fixtures: `order` is live on navigation items and dead only on areas, so each of the eight sites was classified by context before touching it — a blind sweep would have regressed a real feature. After this the only `authorWarn` keys left are the two fail-open area gates in #4651, which need a decision rather than a patch. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E5CYr5SDwe85gH2Jr5KSgu
1 parent f2d308e commit d49fbc8

26 files changed

Lines changed: 547 additions & 213 deletions

File tree

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
---
2+
'@objectstack/spec': major
3+
'@objectstack/example-todo': patch
4+
---
5+
6+
feat(spec)!: retire the six remaining `authorWarn` dead keys — book/group `translations`, `job.id`, `translation.validationMessages`, `app.homePageId`, `app.areas[].order` (#4667)
7+
8+
The #4488 liveness audit marked as `authorWarn` the keys whose *declaration*
9+
actively misleads — not merely unread, but shaped so an author reasonably
10+
concludes they configure something. #4509 and #4583 cleared the rest; these six
11+
are what remained, and each shipped with its own reason for reading alive.
12+
13+
**The retirement kit:**
14+
15+
| FROM | TO | Fix |
16+
|---|---|---|
17+
| `book.translations` | *(removed)* | Delete the key. Localize the **docs**`doc.translations` is live on every doc render path. |
18+
| `book.groups[].translations` | *(removed)* | Same. Tombstoned, since `BookGroupSchema` is not `.strict()`. |
19+
| `job.id` | *(removed)* | Delete the key. `name` is the job's identity everywhere. |
20+
| `translation.validationMessages` | *(removed)* | Delete the key. Author the message on the rule: `object.validations[].message`. |
21+
| `app.homePageId` | *(removed)* | Delete the key. Reorder `navigation`; set `isDefault` for the root landing. |
22+
| `app.areas[].order` | *(removed)* | Delete the key. Reorder the `areas` array itself. |
23+
24+
Run `os migrate meta --from 16` to rewrite existing sources automatically.
25+
26+
**Each read alive for a different reason, and the prescriptions say which:**
27+
28+
- **book `translations`***proximity*. `doc.translations`, two files over, same
29+
name and shape, works on every read path. The book-level map was parsed,
30+
stored and round-tripped, and rendered in the authoring locale to every
31+
reader: the tree endpoint and the portal emit `label` / `description`
32+
verbatim.
33+
- **`job.id`***its own description*. "Defaults to `name` when omitted"
34+
advertises an identity override that does not exist. `name` is the scheduling
35+
key, the `sys_job` row key, and the `JobExecution.jobId` stamp — so two jobs
36+
differing only in `id` were one job declared twice.
37+
- **`translation.validationMessages`***the platform's own signposts, twice*.
38+
The schema example showed a concrete override, and #3778's legacy-key
39+
migration table steered retired `errors:` authors straight into it. **That
40+
guidance entry is rewritten here**: retiring one dead key by pointing at
41+
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.
44+
- **`app.areas[].order`***the sibling that works*. Nav-item `order` really is
45+
sorted; area-level order never was, and both renderers iterate the array as
46+
authored.
47+
48+
**Routes differ, deliberately.** `book.groups[].translations` and
49+
`app.homePageId` are **tombstoned** (`retiredKey`: `never` at compile time, a
50+
prescription at parse time) — the group schema is a plain `z.object`, where a
51+
bare delete would have zod silently strip the key, trading one silent no-op for
52+
another. The other four are strict deletions carrying `guidance`. Retired alias
53+
spellings (`i18n`, `home`, `homepage`, `landingpage`, `sort`) route to the same
54+
prescriptions rather than renaming onto keys that are gone.
55+
56+
Registered as three ADR-0087 D2 conversions (`book-translations-removed`,
57+
`job-id-removed`, `translation-validation-messages-removed`) plus an extension
58+
of `app-dead-authoring-keys-removed`, all wired into the protocol-17 D3 chain.
59+
60+
**Also corrected, both found by the gates rather than by grep:** the published
61+
`objectstack-i18n` skill taught `validationMessages` in a copy-paste example
62+
(an AI reproduces that verbatim), and `examples/app-todo` authored the group in
63+
three locales — where the `en` entries merely duplicated the rule's own text and
64+
the zh-CN / ja-JP translations had never once been rendered.
65+
66+
After this, the only `authorWarn` keys left in the ledger are the two fail-open
67+
area gates tracked in #4651, which need a decision rather than a patch.

content/docs/references/api/protocol.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -862,7 +862,7 @@ const result = AiAgentCapabilitiesSchema.parse(data);
862862
| Property | Type | Required | Description |
863863
| :--- | :--- | :--- | :--- |
864864
| **locale** | `string` || Locale code |
865-
| **translations** | `{ objects?: Record<string, { label?: string; pluralLabel?: string; description?: string; fields?: Record<string, { label?: string; help?: string; placeholder?: string; options?: Record<string, string> }>; … }>; apps?: Record<string, { label: string; description?: string; navigation?: Record<string, { label: string }> }>; messages?: Record<string, string>; validationMessages?: Record<string, string>; … }` || Translation data |
865+
| **translations** | `{ objects?: Record<string, { label?: string; pluralLabel?: string; description?: string; fields?: Record<string, { label?: string; help?: string; placeholder?: string; options?: Record<string, string> }>; … }>; apps?: Record<string, { label: string; description?: string; navigation?: Record<string, { label: string }> }>; messages?: Record<string, string>; globalActions?: Record<string, { label?: string; confirmText?: string; successMessage?: string; params?: Record<string, { label?: string; helpText?: string; placeholder?: string; options?: Record<string, string> }>; … }>; … }` || Translation data |
866866

867867

868868
---

content/docs/references/system/book.mdx

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -60,12 +60,11 @@ const result = BookSchema.parse(data);
6060
| **name** | `string` || Book name (namespace prefix recommended, like every metadata name) |
6161
| **label** | `string` | optional | Display title |
6262
| **description** | `string` | optional | |
63-
| **translations** | `Record<string, { label?: string; description?: string }>` | optional | |
6463
| **slug** | `string` | optional | Portal URL segment; defaults to name sans prefix |
6564
| **icon** | `string` | optional | |
6665
| **order** | `number` | optional | Orders books within the portal |
6766
| **audience** | `'org' \| 'public' \| { permissionSet: string }` | optional | Access audience; defaults to 'org' (inherits package grant) |
68-
| **groups** | `{ key: string; label: string; translations?: Record<string, { label: string }>; order?: number; … }[]` || The spine: ordered sections. Two levels total. |
67+
| **groups** | `{ key: string; label: string; translations?: any; order?: number; … }[]` || The spine: ordered sections. Two levels total. |
6968
| **_lock** | `Enum<'none' \| 'no-overlay' \| 'no-delete' \| 'full'>` | optional | Item-level lock — controls overlay & delete (ADR-0010). |
7069
| **_lockReason** | `string` | optional | Human-readable reason shown when a write is refused by _lock. |
7170
| **_lockSource** | `Enum<'artifact' \| 'package' \| 'env-forced'>` | optional | Layer that set _lock (artifact \| package \| env-forced). |
@@ -116,7 +115,7 @@ Type: `'public'`
116115
| :--- | :--- | :--- | :--- |
117116
| **key** | `string` || Stable group key (used by overrides, deep links, explicit `doc.group`) |
118117
| **label** | `string` || Section title — first-class, i18n-homed |
119-
| **translations** | `Record<string, { label: string }>` | optional | Per-locale label variants |
118+
| **translations** | `any` | optional | [REMOVED] Inline `translations` on a book (and on a book group) was removed in @objectstack/spec 17.0.0 (#4667, ADR-0049) — no resolver ever read it. The book tree endpoint and the docs portal render `label` / `description` verbatim in every locale, so a localized book shipped its authoring-locale strings to every reader. Delete the key. NOTE the near neighbour that DOES work: `doc.translations` is live and read on every doc render path — localize the docs themselves, and the portal picks the reader's locale up from there. Run `os migrate meta --from 16` to rewrite existing sources automatically. |
120119
| **order** | `number` | optional | Order of THIS group within the book |
121120
| **include** | `string \| { tag: string }` | optional | Rule that derives membership (glob or tag) |
122121
| **package** | `string` | optional | Scope the rule to a package id (default: the book package; cross-package via ADR-0048) |

content/docs/references/system/job.mdx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,6 @@ const result = CronScheduleSchema.parse(data);
5656

5757
| Property | Type | Required | Description |
5858
| :--- | :--- | :--- | :--- |
59-
| **id** | `string` | optional | Unique job identifier (defaults to `name` when omitted) |
6059
| **name** | `string` || Job name (snake_case) |
6160
| **label** | `string` | optional | Human-readable label |
6261
| **description** | `string` | optional | Job description / purpose |

content/docs/references/system/translation.mdx

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -156,7 +156,6 @@ Translation data for objects, apps, and UI messages
156156
| **objects** | `Record<string, { label?: string; pluralLabel?: string; description?: string; fields?: Record<string, { label?: string; help?: string; placeholder?: string; options?: Record<string, string> }>; … }>` | optional | Object translations keyed by object name |
157157
| **apps** | `Record<string, { label: string; description?: string; navigation?: Record<string, { label: string }> }>` | optional | App translations keyed by app name |
158158
| **messages** | `Record<string, string>` | optional | UI message translations keyed by message ID |
159-
| **validationMessages** | `Record<string, string>` | optional | Translatable validation error messages keyed by rule name (e.g., `{"discount_limit": "折扣不能超过40%"}`) |
160159
| **globalActions** | `Record<string, { label?: string; confirmText?: string; successMessage?: string; params?: Record<string, { label?: string; helpText?: string; placeholder?: string; options?: Record<string, string> }>; … }>` | optional | Global action translations keyed by action name |
161160
| **dashboards** | `Record<string, { label?: string; description?: string; actions?: Record<string, { label?: string }>; widgets?: Record<string, { title?: string; description?: string }> }>` | optional | Dashboard translations keyed by dashboard name |
162161
| **pages** | `Record<string, { label?: string; description?: string; title?: string; subtitle?: string }>` | optional | Page translations keyed by page name |
@@ -210,7 +209,6 @@ One locale of translations — the `translation` metadata type
210209
| **objects** | `Record<string, { label?: string; pluralLabel?: string; description?: string; fields?: Record<string, { label?: string; help?: string; placeholder?: string; options?: Record<string, string> }>; … }>` | optional | Object translations keyed by object name |
211210
| **apps** | `Record<string, { label: string; description?: string; navigation?: Record<string, { label: string }> }>` | optional | App translations keyed by app name |
212211
| **messages** | `Record<string, string>` | optional | UI message translations keyed by message ID |
213-
| **validationMessages** | `Record<string, string>` | optional | Translatable validation error messages keyed by rule name (e.g., `{"discount_limit": "折扣不能超过40%"}`) |
214212
| **globalActions** | `Record<string, { label?: string; confirmText?: string; successMessage?: string; params?: Record<string, { label?: string; helpText?: string; placeholder?: string; options?: Record<string, string> }>; … }>` | optional | Global action translations keyed by action name |
215213
| **dashboards** | `Record<string, { label?: string; description?: string; actions?: Record<string, { label?: string }>; widgets?: Record<string, { title?: string; description?: string }> }>` | optional | Dashboard translations keyed by dashboard name |
216214
| **pages** | `Record<string, { label?: string; description?: string; title?: string; subtitle?: string }>` | optional | Page translations keyed by page name |

content/docs/references/ui/app.mdx

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -81,9 +81,9 @@ const result = ActionNavItemSchema.parse(data);
8181
| **isDefault** | `boolean` | optional | Is default app |
8282
| **hidden** | `boolean` | optional | Hide from the App Switcher; the shell surfaces hidden apps via the avatar menu instead |
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 |
84-
| **areas** | `{ id: string; label: string; icon?: string; order?: number; … }[]` | optional | Navigation areas for partitioning navigation by business domain |
84+
| **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** | `string` | optional | ID of the navigation item to serve as landing page |
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. |
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. |
@@ -210,7 +210,6 @@ const result = ActionNavItemSchema.parse(data);
210210
| **id** | `string` || Unique area identifier (lowercase snake_case) |
211211
| **label** | `string` || Area display label |
212212
| **icon** | `string` | optional | Area icon name |
213-
| **order** | `number` | optional | Sort order among areas (lower = first) |
214213
| **description** | `string` | optional | Area description |
215214
| **visible** | `string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object }` | optional | Visibility predicate (CEL) for this area. |
216215
| **requiredPermissions** | `string[]` | optional | Permissions required to access this area |

0 commit comments

Comments
 (0)