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
67 changes: 67 additions & 0 deletions .changeset/book-job-translation-app-authorwarn-keys-retired.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
---
'@objectstack/spec': major
'@objectstack/example-todo': patch
---

feat(spec)!: retire the six remaining `authorWarn` dead keys — book/group `translations`, `job.id`, `translation.validationMessages`, `app.homePageId`, `app.areas[].order` (#4667)

The #4488 liveness audit marked 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 shipped with its own reason for reading alive.

**The retirement kit:**

| FROM | TO | Fix |
|---|---|---|
| `book.translations` | *(removed)* | Delete the key. Localize the **docs** — `doc.translations` is live on every doc render path. |
| `book.groups[].translations` | *(removed)* | Same. Tombstoned, since `BookGroupSchema` is not `.strict()`. |
| `job.id` | *(removed)* | Delete the key. `name` is the job's identity everywhere. |
| `translation.validationMessages` | *(removed)* | Delete the key. Author the message on the rule: `object.validations[].message`. |
| `app.homePageId` | *(removed)* | Delete the key. Reorder `navigation`; set `isDefault` for the root landing. |
| `app.areas[].order` | *(removed)* | Delete the key. Reorder the `areas` array itself. |

Run `os migrate meta --from 16` to rewrite existing sources automatically.

**Each read alive for a different reason, and the prescriptions say which:**

- **book `translations`** — *proximity*. `doc.translations`, two files over, same
name and shape, works on every read path. The book-level map was parsed,
stored and round-tripped, and rendered in the authoring locale to every
reader: the tree endpoint and the portal emit `label` / `description`
verbatim.
- **`job.id`** — *its own description*. "Defaults to `name` when omitted"
advertises an identity override that does not exist. `name` is the scheduling
key, the `sys_job` row key, and the `JobExecution.jobId` stamp — so 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 legacy-key
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.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.

**Routes differ, deliberately.** `book.groups[].translations` and
`app.homePageId` are **tombstoned** (`retiredKey`: `never` at compile time, a
prescription at parse time) — the group schema is a plain `z.object`, 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`. Retired alias
spellings (`i18n`, `home`, `homepage`, `landingpage`, `sort`) route to the same
prescriptions rather than renaming onto keys that are gone.

Registered as three ADR-0087 D2 conversions (`book-translations-removed`,
`job-id-removed`, `translation-validation-messages-removed`) plus an extension
of `app-dead-authoring-keys-removed`, all wired into the protocol-17 D3 chain.

**Also corrected, both found by the gates rather than by grep:** the published
`objectstack-i18n` skill taught `validationMessages` in a copy-paste example
(an AI reproduces that verbatim), 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.

After this, the only `authorWarn` keys left in the ledger are the two fail-open
area gates tracked in #4651, which need a decision rather than a patch.
3 changes: 1 addition & 2 deletions content/docs/protocol/kernel/i18n-standard.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -193,7 +193,6 @@ const zh: TranslationData = {
pages: { landing: { title: '欢迎' } },
globalActions: { export_csv: { label: '导出 CSV' } },
messages: { 'common.save': '保存' },
validationMessages: { 'discount_limit': '折扣不能超过40%' },
};
```

Expand Down Expand Up @@ -841,7 +840,7 @@ options: { 'Direct Mail': '直邮' } // ❌ keyed by the label — never resolv
options: { 'direct-mail': '直邮' } // ❌ a variant spelling of the value
```

`messages`, `validationMessages`, `settings`, `settingsCommon` and
`messages`, `settings`, `settingsCommon` and
`metadataForms` are **not** checked: their keys are owned by application code,
plugins, and the platform's own metadata-type registry rather than by this
stack's metadata, so there is no set of legal names to resolve against.
Expand Down
2 changes: 1 addition & 1 deletion content/docs/references/api/protocol.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -862,7 +862,7 @@ const result = AiAgentCapabilitiesSchema.parse(data);
| Property | Type | Required | Description |
| :--- | :--- | :--- | :--- |
| **locale** | `string` | ✅ | Locale code |
| **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 |
| **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 |


---
Expand Down
5 changes: 2 additions & 3 deletions content/docs/references/system/book.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -60,12 +60,11 @@ const result = BookSchema.parse(data);
| **name** | `string` | ✅ | Book name (namespace prefix recommended, like every metadata name) |
| **label** | `string` | optional | Display title |
| **description** | `string` | optional | |
| **translations** | `Record<string, { label?: string; description?: string }>` | optional | |
| **slug** | `string` | optional | Portal URL segment; defaults to name sans prefix |
| **icon** | `string` | optional | |
| **order** | `number` | optional | Orders books within the portal |
| **audience** | `'org' \| 'public' \| { permissionSet: string }` | optional | Access audience; defaults to 'org' (inherits package grant) |
| **groups** | `{ key: string; label: string; translations?: Record<string, { label: string }>; order?: number; … }[]` | ✅ | The spine: ordered sections. Two levels total. |
| **groups** | `{ key: string; label: string; translations?: any; order?: number; … }[]` | ✅ | The spine: ordered sections. Two levels total. |
| **_lock** | `Enum<'none' \| 'no-overlay' \| 'no-delete' \| 'full'>` | optional | Item-level lock — controls overlay & delete (ADR-0010). |
| **_lockReason** | `string` | optional | Human-readable reason shown when a write is refused by _lock. |
| **_lockSource** | `Enum<'artifact' \| 'package' \| 'env-forced'>` | optional | Layer that set _lock (artifact \| package \| env-forced). |
Expand Down Expand Up @@ -116,7 +115,7 @@ Type: `'public'`
| :--- | :--- | :--- | :--- |
| **key** | `string` | ✅ | Stable group key (used by overrides, deep links, explicit `doc.group`) |
| **label** | `string` | ✅ | Section title — first-class, i18n-homed |
| **translations** | `Record<string, { label: string }>` | optional | Per-locale label variants |
| **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. |
| **order** | `number` | optional | Order of THIS group within the book |
| **include** | `string \| { tag: string }` | optional | Rule that derives membership (glob or tag) |
| **package** | `string` | optional | Scope the rule to a package id (default: the book package; cross-package via ADR-0048) |
Expand Down
1 change: 0 additions & 1 deletion content/docs/references/system/job.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,6 @@ const result = CronScheduleSchema.parse(data);

| Property | Type | Required | Description |
| :--- | :--- | :--- | :--- |
| **id** | `string` | optional | Unique job identifier (defaults to `name` when omitted) |
| **name** | `string` | ✅ | Job name (snake_case) |
| **label** | `string` | optional | Human-readable label |
| **description** | `string` | optional | Job description / purpose |
Expand Down
2 changes: 0 additions & 2 deletions content/docs/references/system/translation.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,6 @@ Translation data for objects, apps, and UI messages
| **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 |
| **apps** | `Record<string, { label: string; description?: string; navigation?: Record<string, { label: string }> }>` | optional | App translations keyed by app name |
| **messages** | `Record<string, string>` | optional | UI message translations keyed by message ID |
| **validationMessages** | `Record<string, string>` | optional | Translatable validation error messages keyed by rule name (e.g., `{"discount_limit": "折扣不能超过40%"}`) |
| **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 |
| **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 |
| **pages** | `Record<string, { label?: string; description?: string; title?: string; subtitle?: string }>` | optional | Page translations keyed by page name |
Expand Down Expand Up @@ -210,7 +209,6 @@ One locale of translations — the `translation` metadata type
| **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 |
| **apps** | `Record<string, { label: string; description?: string; navigation?: Record<string, { label: string }> }>` | optional | App translations keyed by app name |
| **messages** | `Record<string, string>` | optional | UI message translations keyed by message ID |
| **validationMessages** | `Record<string, string>` | optional | Translatable validation error messages keyed by rule name (e.g., `{"discount_limit": "折扣不能超过40%"}`) |
| **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 |
| **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 |
| **pages** | `Record<string, { label?: string; description?: string; title?: string; subtitle?: string }>` | optional | Page translations keyed by page name |
Expand Down
5 changes: 2 additions & 3 deletions content/docs/references/ui/app.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -81,9 +81,9 @@ const result = ActionNavItemSchema.parse(data);
| **isDefault** | `boolean` | optional | Is default app |
| **hidden** | `boolean` | optional | Hide from the App Switcher; the shell surfaces hidden apps via the avatar menu instead |
| **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; order?: number; … }[]` | optional | Navigation areas for partitioning navigation by business domain |
| **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** | `string` | optional | ID of the navigation item to serve as landing page |
| **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. |
| **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 Expand Up @@ -210,7 +210,6 @@ const result = ActionNavItemSchema.parse(data);
| **id** | `string` | ✅ | Unique area identifier (lowercase snake_case) |
| **label** | `string` | ✅ | Area display label |
| **icon** | `string` | optional | Area icon name |
| **order** | `number` | optional | Sort order among areas (lower = first) |
| **description** | `string` | optional | Area description |
| **visible** | `string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object }` | optional | Visibility predicate (CEL) for this area. |
| **requiredPermissions** | `string[]` | optional | Permissions required to access this area |
Expand Down
11 changes: 5 additions & 6 deletions content/docs/ui/apps.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -50,10 +50,6 @@ const crmApp = {
| `navigation` | `NavigationItem[]` | optional | Navigation tree |
| `branding` | `AppBranding` | optional | Visual customization |
| `requiredPermissions` | `string[]` | optional | Required permissions to access |
| `homePageId` | `string` | optional | ID of the navigation item to serve as the landing page |
| `objects` | `unknown[]` | optional | Objects belonging to this app |
| `apis` | `unknown[]` | optional | Custom APIs belonging to this app |
| `mobileNavigation` | `object` | optional | Mobile-specific navigation |

## Navigation Items

Expand Down Expand Up @@ -181,7 +177,8 @@ label — the only keys it accepts are `type`, an optional `id`, and an optional

All navigation items **except `separator`** share these base properties. Every
item **must** declare a unique `id` (lowercase `snake_case`) — it is required by
the schema and is referenced by `homePageId` and `mobileNavigation.bottomNavItems`.
the schema, and it is what `order` sorts and what the landing page resolves to
(the first item, since `homePageId` was removed in 17.0.0 — see below).
(On a `separator`, `id` is optional and the remaining properties below are
rejected — a divider has nothing to label, gate, or badge.)

Expand Down Expand Up @@ -286,7 +283,9 @@ const projectApp = {
],

requiredPermissions: ['pm_access'],
homePageId: 'nav_home',
// 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.
};
```

Expand Down
8 changes: 5 additions & 3 deletions content/docs/ui/translations.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -235,9 +235,11 @@ up-to-date bundle re-extracts byte-identically.

Honest limits worth knowing before you plan around them:

- **`validationMessages` has no runtime consumer today.** The schema accepts it
and the coverage tooling counts it, but nothing reads it back — validation
errors are not translated through bundles yet.
- **Validation messages are not translated through bundles.** The
`validationMessages` group was removed in 17.0.0 (#4667): it had no runtime
consumer, so a translated rule message was stored and never shown. Author the
message on the rule itself (`object.validations[].message`), which the engine
returns on every rejected write.
- **No ICU MessageFormat** — plural/gender formatting isn't available;
interpolation is always simple `{variable}` substitution.
- **Runtime authoring is process-wide.** The authored layer is synced across all
Expand Down
Loading
Loading