You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
The i18n gate only ever ran forward: `os i18n check` asks which keys the
metadata expects that no bundle carries. Nothing asked the reverse — which
keys a bundle carries that no metadata claims — though the spec already names
the answer (`TranslationDiffStatus 'redundant'`, `redundantKeys`), both
declared with no producer.
That direction ships two failure modes, both in the HotCRM audit: bundles keyed
to fields an object no longer declares, and option translations keyed by the
display label or a variant spelling of the stored value (`direct-mail` for
`direct_mail`, `planned` for `planning`). Neither breaks anything — the
resolver finds nothing and renders the source string, so the screen looks
translated while one field quietly is not.
`validateTranslationReferences` walks every bundle in `stack.translations`
against the stack it ships with — objects, fields, option values, views,
actions and their params, sections, apps and nav ids, dashboards, widgets,
header actions, global actions — wired into `validate`, `lint` and `compile`.
All findings are warnings: an orphan key is inert, not broken.
Cross-package objects keep the S3 rule: a registered platform object is
skipped wholly (its fields are invisible to a stack lint), an unregistered
platform-prefixed name is reported once on the object key, and no subtree is
half-checked. `messages`, `validationMessages`, `settings`, `settingsCommon`
and `metadataForms` are deliberately unjudged — their keys belong to code,
plugins and the platform registry, so there is no universe to resolve against.
Two example apps carried the exact bug class and are fixed at the producer:
app-crm translated `account_id`/`contact_id`/`opportunity_id` (fields are
`account`/`contact`/`opportunity`), `first_name`/`last_name` on a lead that
declares `name`, two sections nothing declares, and app `crm` (it is
`crm_app`); app-todo keyed every bundle to `task` while the object is
`todo_task` — 92 strings that looked translated were unreachable, which the
coverage ratchet now records. Its completeness test hard-coded the same wrong
key, so it stayed green against a bundle no resolver could find; it now reads
the key from `Task.name`.
Verified: lint 492 / cli 673 tests pass; the rule reports zero findings across
app-crm, app-showcase and app-todo after the fixes, having visited 274 field
keys, 94 option keys and 5 app keys over 35 object nodes — a floor with
substance, not an empty walk.
Refs #3583
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015r31hzuSN19iK8ATRPcCpS
Copy file name to clipboardExpand all lines: docs/audits/2026-07-app-metadata-reference-integrity-assessment.md
+1-1Lines changed: 1 addition & 1 deletion
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -90,7 +90,7 @@ Effort legend: **S** ≤ ~1 day · **M** 2–4 days · **L** ≥ 1 week. Every r
90
90
| Rule | Covers | Notes | Effort |
91
91
|---|---|---|---|
92
92
| R5 `validate-nav-access`| 8 |`buildAccessMatrix` × app nav (own, non-platform objects only): nav-exposed object where no permission set grants read → **warning** (grants may come from another package's set, `adminScope`, or the superuser wildcard — advisory is the honest ceiling, per §2.1 S4 precedent). Optionally also "app hidden by `tabPermissions` for every set". First actual lint consumer of `buildAccessMatrix`. | M |
93
-
| R6 `validate-translation-references`| 9 | The reverse/orphan direction the spec already names (`'redundant'`): walk bundle keys → objects/fields/views/actions must exist; option sub-keys must be option **values** (flag a key that matches a label or is an edit-distance near-miss of a value — the `direct-mail` vs `direct_mail`, `planned` vs `planning` class, with did-you-mean hints). Pure `(stack) => Finding[]`, so it belongs in `@objectstack/lint`, not the CLI util. **warning** (orphan keys are inert, not broken). | M/L (key-shape diversity: both `TranslationDataSchema` and the object-first `AppTranslationBundleSchema`) |
93
+
| R6 `validate-translation-references` | 9 | **Landed 2026-07-28.** The reverse/orphan direction the spec already names (`'redundant'`): bundle keys → objects/fields/views/actions/sections/action-params, apps + nav ids, dashboards + widget ids + header `actionUrl`, `globalActions`; option sub-keys must be option **values** (a key that matches a display label is named as such; near-misses get did-you-mean, plus a namespace-segment pass so `task` suggests `todo_task`). **warning** throughout (orphan keys are inert, not broken). Cross-package objects follow the §4 ladder with S3 intact: a registered platform object is skipped WHOLLY, since its fields are not visible from here. Scope note: `TranslationDataSchema` only — the object-first `AppTranslationBundleSchema` is the `translation` METADATA TYPE, not `stack.translations`, so its keys are skipped rather than reported; `messages`/`validationMessages`/`settings`/`settingsCommon`/`metadataForms` are deliberately unjudged (their keys are owned by code, plugins and the platform registry, so there is no stack-enumerable universe). | M/L → shipped M |
94
94
| R7 `validate-ai-references`| 6 (the resolvable subset) |`agent.skills` → stack.skills; `skill.tools` (wildcard-aware) → stack.tools; `agent.tools[].name` → actions/flows by declared type. Kernel-provided skills/tools (`ask`/`build` register theirs at runtime) need either inclusion in the Phase-1 registry or S2 advisory severity — resolve with D2. `knowledge.indexes`/`sources` is **blocked on D1** — do not lint a namespace that has no definition site; that would institutionalise the gap. | M + D1/D2 |
95
95
| R8 (Tier-B candidate) option-value literals in metadata | — | kanban group values, `ChartConfig.colors` keys, filter literals vs. select options. High false-positive surface (filters legitimately hold dynamic/user values) — per the ADR-0078 sharing-rule lesson, this gets a verification note *before* it becomes a rule, or it doesn't ship. | ? |
0 commit comments