Repository navigation
Commit ee42f00
feat(lint): os validate judges every keyed child of an action translation entry against its declaration (#21258)
Fixes #21216
Clause-②: yes
## What changed
`validateTranslationReferences`
(`packages/lint/src/validate-translation-references.ts`) is the rule
behind `translation-target-unknown`, and `os validate`, `os build` and
`os lint` all run it. It now judges every keyed child of an action's
translation entry against what the action declares. Both addresses are
covered: `objects.OBJECT._actions.ACTION` and `globalActions.ACTION`.
Before this change the walk (`checkActionParams`) judged `params.NAME`
and nothing else.
| bundle key | judged against | code · level | the runtime read it
mirrors |
|---|---|---|---|
| `params.NAME` | `params[].name`, falling back to `field` |
`translation-target-unknown` · error (unchanged) |
`translateActionParams` |
| `outcomeMessages.OUTCOME` | the keys of the action's `outcomeMessages`
| `translation-target-unknown` · error (**new**) |
`resolveActionOutcomeMessages` |
| `resultDialog.fields.PATH` | each literal `resultDialog.fields[].path`
| `translation-target-unknown` · error (**new**) |
`resolveActionResultDialog`, and objectui's `actionResultDialog` |
| `params.NAME.options.VALUE` | a declared param's inline
`options[].value` | `translation-option-key-unknown` · warning (**new**)
| `translateActionParams`, and objectui's `actionParamOptionLabel` |
- **No second vocabulary.** Undeclared outcomes and result-field paths
reuse `params`' own code and severity. Option keys reuse the code and
severity a field's `options` key already gets (`checkOptionKeys`),
including the "keyed by the display label, rename it to the value"
diagnosis. The module's Severity note explains why option keys stay
warnings: such a key names something real, and the fix is a rename.
- **Where the declared sets come from (runtime read sites, on
`packages/spec/src/system/i18n-resolver.ts`):**
- `resolveActionOutcomeMessages` loops over
`Object.keys(action.outcomeMessages)`. When the action declares no
`outcomeMessages`, it returns `undefined` before reading any bundle key.
So for such an action every bundle outcome is unread, and the lint
refuses each one.
- `resolveActionResultDialog` maps over the declared `fields[]` and
looks up `fields[field.path]`. With no `resultDialog` it reads nothing.
With a dialog that declares no `fields`, it reads no label. objectui's
`useObjectLabel.actionResultDialog` uses the same keys.
- Param options are judged only against an **inline** `options` array.
The spec resolver overlays only that array. objectui resolves
`param.options` first and falls back to the referenced field's options,
so an authored array wins. A field-backed param with no inline array
gets its list from the field when the dialog renders. This rule does not
re-derive that inheritance, so those keys are deliberately left
unjudged.
- A result-field path is one literal key, dots included, so the
finding's config path brackets it:
`resultDialog.fields["client.ghost"]`. `localePath` now uses the same
`memberPath` helper, and its output is byte-identical.
- **Coverage pin.** The test file gets a twin of the object-branch
coverage pin. It reads the action translation shape off
`ObjectTranslationDataSchema` and `TranslationDataSchema` (both
addresses), down into `params.NAME.*` and `resultDialog.*`, and holds
that key set equal to a classification ledger. Every `reference-checked`
key needs a ghost that reports at both addresses. When the next keyed
group joins the shape with no leg, this pin fails. That gap is how
`outcomeMessages` shipped unjudged.
## Measured at the public door (`os validate --json`)
The probe stack is a `defineStack` app with `delete_probe` (bound,
declares outcome `archived`, params `reason`, and `mode` with inline
option `fast`), `reveal_probe` (bound, `resultDialog.fields[].path` =
`client.secret`), `check_probe` (object-less, outcome `up_to_date`) and
`reveal_global` (object-less, path `token`). Its `zh-CN` bundle carries
each declared key and one undeclared sibling. The CLI closure was built
at each tree.
| key in the bundle | before, `be5a83cf` (clean `main`) | after,
`ac847edd` / `dd39cf54` |
|---|---|---|
| `_actions.reveal_probe.resultDialog.fields["client.ghost"]` | no
finding | `translation-target-unknown` error |
| `globalActions.reveal_global.resultDialog.fields.ghost_token` | no
finding | `translation-target-unknown` error |
| `_actions.delete_probe.outcomeMessages.restored` | no finding |
`translation-target-unknown` error |
| `globalActions.check_probe.outcomeMessages.stale` | no finding |
`translation-target-unknown` error |
| `_actions.delete_probe.params.mode.options.slow` | no finding |
`translation-option-key-unknown` warning |
| `_actions.delete_probe.params.not_a_param` (control: the door is
reached) | `translation-target-unknown` error | unchanged |
| declared `archived`, `up_to_date`, `client.secret`, `token`, `fast`,
`reason` (controls) | no finding | no finding |
## Consumers whose verdict could move (direction: downstream of
`@objectstack/lint`)
- `os validate --json` over the four example apps, after the change:
`app-crm` exit 0, `app-todo` exit 0, `app-showcase` exit 0,
`app-multi-package` exit 0. Each has **0** findings on an
`outcomeMessages`, `resultDialog.fields` or `params.NAME.options` path,
and 0 translation-rule findings at all. These legs judged nothing before
this change, so any finding on those paths would have been new.
- The bundle `@objectstack/platform-objects` ships
(`SetupAppTranslations`: 4 locales, 50 objects) was run through the
rule. It carries 64 `resultDialog.fields` keys and 44 param-option keys,
and none of them is flagged. It carries 0 outcome keys. Docs and
`skills/**` hold no translation bundle with these keys. No published
bundle needed a fix.
## Tests
All runs are at `ac847edd`. The lint source in `dd39cf54` is
byte-identical; that commit adds only the changeset.
- `pnpm --filter @objectstack/lint test`: 119 files / **5517 passed**.
- `pnpm --filter @objectstack/lint typecheck` (tsc plus
`check:test-typecheck`): exit 0. The test-layer ledger holds 2 files / 6
errors, unchanged.
- `pnpm --filter @objectstack/cli exec vitest run --project unit`, at
`dd39cf54`, in two shards: 122 files / 1861 passed, then 121 files /
1578 passed. The `integration` tier is left to CI, because no
integration-tier file or spawn entry is touched.
- New in `validate-translation-references.test.ts`: 15 cases. An
undeclared outcome is refused at `_actions.ACTION` and at
`globalActions.ACTION`. Declared outcomes pass at both addresses (the
control). An action with no `outcomeMessages` has every outcome refused.
The same three shapes cover `resultDialog.fields`, including the
bracketed dotted path. Param options cover a value miss, a label key, a
param with neither options nor field, the field-backed skip, and an
undeclared param reported once. The coverage pin adds 3 cases.
**Reverse verification** (committed first, then mutated through
`scripts/ablation-replace.mjs` in WRAP mode, with a restore trap; every
restore was proven by blob hash equal to `HEAD` and an empty `git diff
HEAD`):
- Leg A drops the `outcomeMessages` and `resultDialog.fields` calls: **7
failed** / 142 passed. Those are the 6 refusal cases plus the coverage
pin's "backs every reference-checked claim". The 2 control cases stay
green.
- Leg B disables the param-options call: **3 failed** / 146 passed.
Those are the 2 option warnings plus the coverage pin.
- The direction is turned red, as expected. The suite reads `src`
through a relative import, so no `dist/` leg applies. The door probe's
`@objectstack/lint` dist was checked with
`scripts/ablation-dist-preflight.mjs @objectstack/lint
checkActionResultDialogFields`: the marker is present in 4 built files
and the tree is clean.
## Gates
`node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack
--commands` derived 60 families from the merge base `be5a83cf`, and all
60 were run at `dd39cf54`. `--ran` reports **60 run, 0 NOT-MEASURED, 0
UNRUN**, and every exit is 0. `check:dual-build-cjs-loads` first exited
3 (PREREQUISITE NOT MET: 8 unrelated packages had no `dist/`). After a
turbo build of those 8, it was re-run and exited 0. The derivation
flagged a stale tree: `origin/main` is 4 commits ahead, and
`scripts/check-route-envelope.mjs` and
`scripts/engine-double-contract.pinned.json` changed upstream. Neither
family reads the files this PR edits. The 6 value-bearing CI-only
invocations (shard attestation, test completeness,
`check-issue-citations --census`) are NOT MEASURED here because they are
CI-only.
ESLint was narrowed to the two edited files. Their population comes from
eslint's own config: `--print-config` resolves a config for each. The
JSON output reports 2 files, 0 errors and 0 warnings. Type-aware linting
is not enabled (`eslint.config.mjs` sets no `parserOptions.project`), so
this diff cannot change the verdict on any untouched file.
## Acceptance notes
Both of these were measured at the door with a second probe and reported
to the PM rather than fixed here.
- A bound action's `globalActions.ACTION` key is refused with "This key
is never read". `translateAction` (via `lookupActionField`) and
objectui's `actionSuffixes` both fall back to `globalActions.ACTION` for
a bound action, and that probe's label resolves. So the lint's message
and the resolvers disagree, and this PR touches neither side.
- `_actions.ACTION.resultDialog.title`, `.description` and
`.acknowledge` on an action that declares no `resultDialog` pass
silently, and nothing reads them. These are leaf copy on an undeclared
node, not a keyed child, so the coverage pin classifies `resultDialog`
as a container and this PR leaves them unjudged.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01UtnxvdiN376GF3sgXwAw4d)_
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent 7923c8e commit ee42f00
3 files changed
Lines changed: 604 additions & 22 deletions
File tree
- .changeset
- packages/lint/src
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
4 | 4 | | |
5 | 5 | | |
6 | 6 | | |
7 | | - | |
| 7 | + | |
8 | 8 | | |
9 | 9 | | |
10 | 10 | | |
| |||
3073 | 3073 | | |
3074 | 3074 | | |
3075 | 3075 | | |
| 3076 | + | |
| 3077 | + | |
| 3078 | + | |
| 3079 | + | |
| 3080 | + | |
| 3081 | + | |
| 3082 | + | |
| 3083 | + | |
| 3084 | + | |
| 3085 | + | |
| 3086 | + | |
| 3087 | + | |
| 3088 | + | |
| 3089 | + | |
| 3090 | + | |
| 3091 | + | |
| 3092 | + | |
| 3093 | + | |
| 3094 | + | |
| 3095 | + | |
| 3096 | + | |
| 3097 | + | |
| 3098 | + | |
| 3099 | + | |
| 3100 | + | |
| 3101 | + | |
| 3102 | + | |
| 3103 | + | |
| 3104 | + | |
| 3105 | + | |
| 3106 | + | |
| 3107 | + | |
| 3108 | + | |
| 3109 | + | |
| 3110 | + | |
| 3111 | + | |
| 3112 | + | |
| 3113 | + | |
| 3114 | + | |
| 3115 | + | |
| 3116 | + | |
| 3117 | + | |
| 3118 | + | |
| 3119 | + | |
| 3120 | + | |
| 3121 | + | |
| 3122 | + | |
| 3123 | + | |
| 3124 | + | |
| 3125 | + | |
| 3126 | + | |
| 3127 | + | |
| 3128 | + | |
| 3129 | + | |
| 3130 | + | |
| 3131 | + | |
| 3132 | + | |
| 3133 | + | |
| 3134 | + | |
| 3135 | + | |
| 3136 | + | |
| 3137 | + | |
| 3138 | + | |
| 3139 | + | |
| 3140 | + | |
| 3141 | + | |
| 3142 | + | |
| 3143 | + | |
| 3144 | + | |
| 3145 | + | |
| 3146 | + | |
| 3147 | + | |
| 3148 | + | |
| 3149 | + | |
| 3150 | + | |
| 3151 | + | |
| 3152 | + | |
| 3153 | + | |
| 3154 | + | |
| 3155 | + | |
| 3156 | + | |
| 3157 | + | |
| 3158 | + | |
| 3159 | + | |
| 3160 | + | |
| 3161 | + | |
| 3162 | + | |
| 3163 | + | |
| 3164 | + | |
| 3165 | + | |
| 3166 | + | |
| 3167 | + | |
| 3168 | + | |
| 3169 | + | |
| 3170 | + | |
| 3171 | + | |
| 3172 | + | |
| 3173 | + | |
| 3174 | + | |
| 3175 | + | |
| 3176 | + | |
| 3177 | + | |
| 3178 | + | |
| 3179 | + | |
| 3180 | + | |
| 3181 | + | |
| 3182 | + | |
| 3183 | + | |
| 3184 | + | |
| 3185 | + | |
| 3186 | + | |
| 3187 | + | |
| 3188 | + | |
| 3189 | + | |
| 3190 | + | |
| 3191 | + | |
| 3192 | + | |
| 3193 | + | |
| 3194 | + | |
| 3195 | + | |
| 3196 | + | |
| 3197 | + | |
| 3198 | + | |
| 3199 | + | |
| 3200 | + | |
| 3201 | + | |
| 3202 | + | |
| 3203 | + | |
| 3204 | + | |
| 3205 | + | |
| 3206 | + | |
| 3207 | + | |
| 3208 | + | |
| 3209 | + | |
| 3210 | + | |
| 3211 | + | |
| 3212 | + | |
| 3213 | + | |
| 3214 | + | |
| 3215 | + | |
| 3216 | + | |
| 3217 | + | |
| 3218 | + | |
| 3219 | + | |
| 3220 | + | |
| 3221 | + | |
| 3222 | + | |
| 3223 | + | |
| 3224 | + | |
| 3225 | + | |
| 3226 | + | |
| 3227 | + | |
| 3228 | + | |
| 3229 | + | |
| 3230 | + | |
| 3231 | + | |
| 3232 | + | |
| 3233 | + | |
| 3234 | + | |
| 3235 | + | |
| 3236 | + | |
| 3237 | + | |
| 3238 | + | |
| 3239 | + | |
| 3240 | + | |
| 3241 | + | |
| 3242 | + | |
| 3243 | + | |
| 3244 | + | |
| 3245 | + | |
| 3246 | + | |
| 3247 | + | |
| 3248 | + | |
| 3249 | + | |
| 3250 | + | |
| 3251 | + | |
| 3252 | + | |
| 3253 | + | |
| 3254 | + | |
| 3255 | + | |
| 3256 | + | |
| 3257 | + | |
| 3258 | + | |
| 3259 | + | |
| 3260 | + | |
| 3261 | + | |
| 3262 | + | |
| 3263 | + | |
| 3264 | + | |
| 3265 | + | |
| 3266 | + | |
| 3267 | + | |
| 3268 | + | |
| 3269 | + | |
| 3270 | + | |
| 3271 | + | |
| 3272 | + | |
| 3273 | + | |
| 3274 | + | |
| 3275 | + | |
| 3276 | + | |
| 3277 | + | |
| 3278 | + | |
| 3279 | + | |
| 3280 | + | |
| 3281 | + | |
| 3282 | + | |
| 3283 | + | |
| 3284 | + | |
| 3285 | + | |
| 3286 | + | |
| 3287 | + | |
| 3288 | + | |
| 3289 | + | |
| 3290 | + | |
| 3291 | + | |
| 3292 | + | |
| 3293 | + | |
| 3294 | + | |
| 3295 | + | |
| 3296 | + | |
| 3297 | + | |
| 3298 | + | |
| 3299 | + | |
| 3300 | + | |
| 3301 | + | |
| 3302 | + | |
| 3303 | + | |
| 3304 | + | |
| 3305 | + | |
| 3306 | + | |
| 3307 | + | |
| 3308 | + | |
| 3309 | + | |
| 3310 | + | |
| 3311 | + | |
| 3312 | + | |
| 3313 | + | |
| 3314 | + | |
| 3315 | + | |
| 3316 | + | |
| 3317 | + | |
| 3318 | + | |
| 3319 | + | |
| 3320 | + | |
| 3321 | + | |
| 3322 | + | |
| 3323 | + | |
| 3324 | + | |
| 3325 | + | |
| 3326 | + | |
| 3327 | + | |
| 3328 | + | |
| 3329 | + | |
| 3330 | + | |
| 3331 | + | |
| 3332 | + | |
| 3333 | + | |
| 3334 | + | |
| 3335 | + | |
| 3336 | + | |
| 3337 | + | |
| 3338 | + | |
| 3339 | + | |
| 3340 | + | |
| 3341 | + | |
| 3342 | + | |
| 3343 | + | |
| 3344 | + | |
| 3345 | + | |
| 3346 | + | |
| 3347 | + | |
| 3348 | + | |
| 3349 | + | |
| 3350 | + | |
| 3351 | + | |
| 3352 | + | |
| 3353 | + | |
| 3354 | + | |
| 3355 | + | |
| 3356 | + | |
| 3357 | + | |
| 3358 | + | |
| 3359 | + | |
| 3360 | + | |
| 3361 | + | |
| 3362 | + | |
| 3363 | + | |
| 3364 | + | |
| 3365 | + | |
| 3366 | + | |
| 3367 | + | |
| 3368 | + | |
| 3369 | + | |
| 3370 | + | |
| 3371 | + | |
| 3372 | + | |
| 3373 | + | |
| 3374 | + | |
| 3375 | + | |
| 3376 | + | |
| 3377 | + | |
| 3378 | + | |
| 3379 | + | |
| 3380 | + | |
| 3381 | + | |
| 3382 | + | |
| 3383 | + | |
| 3384 | + | |
| 3385 | + | |
| 3386 | + | |
| 3387 | + | |
| 3388 | + | |
| 3389 | + | |
| 3390 | + | |
| 3391 | + | |
| 3392 | + | |
| 3393 | + | |
| 3394 | + | |
| 3395 | + | |
| 3396 | + | |
| 3397 | + | |
| 3398 | + | |
| 3399 | + | |
| 3400 | + | |
| 3401 | + | |
| 3402 | + | |
| 3403 | + | |
| 3404 | + | |
| 3405 | + | |
| 3406 | + | |
| 3407 | + | |
| 3408 | + | |
| 3409 | + | |
| 3410 | + | |
| 3411 | + | |
| 3412 | + | |
| 3413 | + | |
0 commit comments