Commit c88fa2c
Closes #16310
Clause-②: yes
`validate-translation-references` already found every orphan locale key,
named its
id, named its locale and printed the remedy — and failed nothing. This
makes it
fail. Severity only; the rule's detection logic is untouched.
## The ruling this executes
Decision batch #91, director seat (comment 5583985173) — **option A**,
verbatim:
> **Ruled.** An orphaned locale key is dead data that actively misleads
— grepping
> it returns a confident hit in every locale — and a rule that reports
it precisely
> while exiting 0 is declared-but-unenforced. The severity goes to
`error` at the
> three hard-coded sites; `os lint` then fails on it like the forward
> `i18n/missing-*` half already does. ⛔ B / D refused … ⛔ C refused (a
195-id
> migration across two producers for one rule); ⛔ E refused …
The card lists two independent causes and does not order them. **Only
cause 1
(severity) is moved.** Cause 2 (the bare, un-namespaced rule id) is
option C, which
the ruling refused: `packages/lint` exports 200 rule-id constants and
195 are bare,
so prefixing this one makes it the sixth exception or forces a
cross-producer
migration — and renaming a published finding id is itself breaking.
### A consumer really can select this rule by its id (asked for, since
only severity moved)
Two readings, both on this tree:
1. **Exact-id selection works and is the published contract.** The
registry finding
reaches the JSON report as `rule: "translation-target-unknown"`, so
`jq '.issues[] | select(.rule == "translation-target-unknown")'` selects
it
today. Pinned in `validate-translation-references.test.ts` against the
literal
string as well as the exported constant, so a rename cannot pass the pin
by
moving the constant alone.
2. **After this change no selection is needed at all.** The rule fails
the run by
default, on the plain `os lint` / `os validate` / `os build` exit code,
with no
flag and no config. That is the acceptance criterion, and it is met on
the
default path rather than on an opt-in one.
What a consumer still cannot do is reach this rule by *family prefix*.
That is
exactly the asymmetry the card named, and it stays — by ruling, not by
oversight.
## Measured on this tree (⛔ the card is not cited as evidence)
`examples/app-crm`, 8 orphan locale keys planted across both locale
bundles
(`apps.crm_app.navigation`, `objects.crm_lead._sections`,
`objects.crm_lead._views`,
`objects.crm_lead.fields`), `objectstack lint --json`. Planted,
measured, restored;
the restore is proven by blob-hash identity against `HEAD`, not by an
exit code.
The card's shape reproduces: baseline clean, +8 findings when planted,
`passed: true`,
exit 0 — and the pipeline was green throughout.
| tree | total | errors | warnings | `passed` | exit |
| :-- | --: | --: | --: | :-- | --: |
| **before this PR** | | | | | |
| baseline | 12 | 0 | 10 | `true` | 0 |
| 8 orphan keys planted | 20 | 0 | 18 | `true` | **0** |
| restored | 12 | 0 | 10 | `true` | 0 |
| **after this PR** | | | | | |
| baseline | 12 | 0 | 10 | `true` | 0 |
| 8 orphan keys planted | 20 | **8** | 10 | `false` | **1** |
| restored | 12 | 0 | 10 | `true` | 0 |
(`--skip-i18n`, which suppresses the coverage walk but not this rule, so
the table
stays readable. Without it the same run reads 113 / 121 / 113 total with
the same
0 → 8 error delta and the same exit-code flip.)
All three authoring commands move together on the planted tree:
| command | before | after |
| :-- | --: | --: |
| `os lint` | exit 0 | exit 1 |
| `os validate` | exit 0 | exit 1 |
| `os build` | exit 0 | exit 1 |
### ⭐ Negative control — a clean tree is unchanged, no new noise
On the pristine tree the `os lint --json` report is **identical field
for field
before and after**, with one exception: `duration` (wall clock). Checked
on three
clean runs (`baseline`, `baseline --skip-i18n`, `restored`): same
`total`, same
`errors`, same `warnings`, same `passed`, same `issues` array, same exit
code 0.
That identity is structural, not lucky: on a clean tree this rule
returns zero
findings, so the severity literal this PR changes is never reached.
### ⛔ Not "promote all warnings" — 1 rule of 13
Measured on the planted tree, which carries findings from **13 distinct
rules**:
- rules whose severity set changed: **1** —
`translation-target-unknown`, `warning` → `error`
- rules unchanged: **12**
- the finding set is **identical modulo that one severity** (same count,
same paths,
same message and hint text: 121 findings before, 121 after)
`translation-option-key-unknown`, raised by the *same function*,
deliberately stays
`warning`: a mis-keyed option translation names something real and its
remedy is a
rename, not a deletion. `validateTranslatableSections` — the sibling
asking "is there
a key at all?" — is untouched; its comment claiming it is warning-only
*"for the same
reason its sibling is"* was corrected, since that reason no longer
holds.
### The runtime publish gate is unaffected — a measured zero
`validateTranslationReferences` reaches the runtime door on a `flow`
write (default
`runtimeTypes`), so this could have been a refusal widening at the
hottest door.
It is not: the per-write snapshot carries only `objects` / `permissions`
/ `books` /
`datasets`, and `RuntimeStackContext` has no `translations` member for a
host to
fill, so the rule sees no bundle and returns nothing there.
Measured: a `flow` write through `runRuntimeAuthoringRules` returns **0
errors and
0 advisories** from this rule, with `validateReferenceIntegrity`
confirmed in
`rulesRun`, and `buildRuntimeWriteSnapshots(...).baseline` confirmed to
carry no
`translations` key. No publish that used to succeed is refused.
## ⭐ Reverse-read — which existing sentence does this make false?
Six live sentences, all repaired in this PR:
1. `validate-translation-references.ts` module note: *"All findings are
**warnings**.
An orphan key is inert, not broken."* — rewritten; the inertness reading
is what
the card measured wrong, and the new note says why, and why ADR-0072 D1
keeps the
promotion narrow instead of licensing the neighbours.
2. `TranslationRefFinding.severity` doc: *"Always `warning` …"* —
rewritten to state
the split.
3. `TranslationRefSeverity = 'warning'` — widened to `'warning' |
'error'`.
4. The nested-screen comment calling a missed screen *"a
warning-severity false
positive"* — that false positive now fails the run; the comment says so.
5. `reference-integrity-suite.ts` on `validateTranslatableSections`:
*"warning-only
for the same reason its sibling is"* — the sibling no longer is.
Re-grounded on
its own reading (the surface is present; only its heading stays in the
source locale).
6. Eight test assertions pinning `severity: 'warning'` — **re-judged in
place, ⛔ not
deleted**, with the reason recorded in a block comment at the head of
the file.
That silence was deliberate and the note says what it encoded and why it
was wrong.
**Reported zeros** — swept and found nothing to change:
- `content/docs/**`: **0** mentions of this rule id.
- `packages/lint/README.md`, `packages/cli/README.md`: **0** mentions.
- `packages/cli/src/**`: **0** sentences about this rule's severity (the
CLI maps
`f.severity` through generically; `error` passes through untouched).
- `docs/audits/2026-07-…-reference-integrity-assessment.md`: mentions
the rule in a
historical findings table with no severity claim — **not** falsified.
- `examples/app-showcase/test/seed.test.ts`: already says the rule
*"fails on a bundle
entry no section declares"* — **not** falsified, and now literally true.
- `reference-integrity-suite.test.ts:352`: asserts id membership only —
**not** falsified.
- In-repo stacks that would newly go red: **0**. `examples/app-crm`,
`examples/app-todo`
and `examples/app-multi-package` each report **0** `translation-*`
findings.
(`examples/app-showcase` could not be linted in this container — it
fails to load on
a missing `@objectstack/connector-mcp` dist, a build-ordering condition
unrelated to
this diff. Declared to CI.)
**One falsified sentence is NOT repaired here, deliberately:**
`skills/objectstack-i18n/SKILL.md:197` says these commands *"report it
as **warnings**
(`translation-target-unknown`, `translation-option-key-unknown`)"* —
half of that is
now false. `skills/**` is out of this PR's landing scope by dispatch,
and it is a
governed surface with its own seat. Flagged for routing rather than
edited.
## Ablation — the new pins can fail
Reverting the severity constant to `'warning'` in source (on-disk proof:
injected
spelling `grep -c` = 1, replaced spelling `grep -c` = 0) turns the
rule's test file
**red — 9 failed / 58 passed, exit 1**. Restoring (blob hash equal to
`HEAD`,
`git diff HEAD` empty, mutant `grep -c` = 0) returns it to **67 passed,
exit 0**.
The tests import the rule by relative path, so no `dist/` is in that
loop; the
`dist/`-mediated statement is the CLI table above, measured across a
real rebuild.
## Changeset
`@objectstack/lint` **`minor`** (⛔ no `skip-changeset` — a `Clause-②:
yes` PR takes
at least `minor`), carrying the `**BREAKING**` banner with its
before/after and the
one-line author remedy, plus the ADR-0087 disposition
(`not-required (no-migration-prescription)`), verified by
`pnpm check:adr-0087-registration` — exit 0, the disposition echoed on
the pass path.
⚠️ **Worth a reviewer's eye.** The first draft framed the exit-code
delta with the
`FROM → TO` prescription template. The gate refused it, correctly: a
body carrying a
migration prescription contradicts `no-migration-prescription`, and none
of the other
four categories is honest here (`unpublished` — lint publishes;
`already-registered` —
no entry covers this; `runtime-interface-only` — inherits the same
refusal;
`type-surface-only` — requires an `any`/`unknown` base-side reading, and
`TranslationRefSeverity` was concrete at base). The changeset now states
the delta as
what it is — a measured before/after of the tool's own verdict — because
it is not a
migration: no authorable key moves, an orphan key resolved to nothing
before this
release and resolves to nothing after it, and the rule has printed each
one with its
remedy in every release that shipped it. ⛔ The `**BREAKING**` token was
not dropped.
If a reviewer reads that as a category gap rather than a mislabel on my
part, it is
the #13080 shape one axis over (a published *verdict* narrowing) and
wants its own card.
## Verification
- `pnpm --filter @objectstack/lint test` — **101 files, 3749 tests, all
pass**
- `pnpm --filter @objectstack/lint --filter @objectstack/cli typecheck`
— both `Done`
- Derived gate family (`scripts/pm/dispatch-gates.mjs --commands`, 59
commands):
**58 exit 0**. The one non-zero is `check:dual-build-cjs-loads` **exit 3
—
`PREREQUISITE NOT MET`**, which prints *"⛔ This is NOT a pass: nothing
was
measured"*: it reads built output and several unrelated packages have no
`dist/`
in this container. Not measured, declared to CI.
- `pnpm lint` equivalent run in full, not narrowed: `eslint .
--no-inline-config`
over **6640 files — 0 errors, 0 warnings**, at `29bbb886`.
## Acceptance notes
- ⛔ Auto-merge not armed; PR is a draft.
- Assignee and the `Claim:` comment were placed by the PM; neither was
written or
changed here, and no second claim was posted.
---
_Generated by [Claude
Code](https://claude.ai/code/session_012GKcPZbMoGq7WPzKLfRBTU)_
---------
Co-authored-by: Claude <noreply@anthropic.com>
1 parent 7f62536 commit c88fa2c
4 files changed
Lines changed: 218 additions & 21 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 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
387 | 387 | | |
388 | 388 | | |
389 | 389 | | |
390 | | - | |
391 | | - | |
| 390 | + | |
| 391 | + | |
| 392 | + | |
| 393 | + | |
| 394 | + | |
392 | 395 | | |
393 | 396 | | |
394 | 397 | | |
| |||
Lines changed: 100 additions & 8 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
18 | 18 | | |
19 | 19 | | |
20 | 20 | | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
21 | 48 | | |
22 | 49 | | |
23 | 50 | | |
| |||
67 | 94 | | |
68 | 95 | | |
69 | 96 | | |
70 | | - | |
| 97 | + | |
71 | 98 | | |
72 | 99 | | |
73 | 100 | | |
| |||
128 | 155 | | |
129 | 156 | | |
130 | 157 | | |
131 | | - | |
| 158 | + | |
132 | 159 | | |
133 | 160 | | |
134 | 161 | | |
| |||
266 | 293 | | |
267 | 294 | | |
268 | 295 | | |
| 296 | + | |
| 297 | + | |
| 298 | + | |
| 299 | + | |
| 300 | + | |
| 301 | + | |
| 302 | + | |
| 303 | + | |
| 304 | + | |
| 305 | + | |
| 306 | + | |
| 307 | + | |
| 308 | + | |
| 309 | + | |
| 310 | + | |
| 311 | + | |
| 312 | + | |
| 313 | + | |
| 314 | + | |
| 315 | + | |
| 316 | + | |
| 317 | + | |
| 318 | + | |
| 319 | + | |
| 320 | + | |
| 321 | + | |
| 322 | + | |
| 323 | + | |
| 324 | + | |
| 325 | + | |
| 326 | + | |
| 327 | + | |
| 328 | + | |
| 329 | + | |
| 330 | + | |
| 331 | + | |
| 332 | + | |
| 333 | + | |
| 334 | + | |
| 335 | + | |
| 336 | + | |
| 337 | + | |
| 338 | + | |
| 339 | + | |
| 340 | + | |
| 341 | + | |
| 342 | + | |
| 343 | + | |
| 344 | + | |
| 345 | + | |
| 346 | + | |
| 347 | + | |
| 348 | + | |
| 349 | + | |
| 350 | + | |
| 351 | + | |
| 352 | + | |
| 353 | + | |
| 354 | + | |
| 355 | + | |
| 356 | + | |
| 357 | + | |
| 358 | + | |
| 359 | + | |
| 360 | + | |
269 | 361 | | |
270 | 362 | | |
271 | 363 | | |
| |||
398 | 490 | | |
399 | 491 | | |
400 | 492 | | |
401 | | - | |
| 493 | + | |
402 | 494 | | |
403 | 495 | | |
404 | 496 | | |
| |||
585 | 677 | | |
586 | 678 | | |
587 | 679 | | |
588 | | - | |
| 680 | + | |
589 | 681 | | |
590 | 682 | | |
591 | 683 | | |
| |||
598 | 690 | | |
599 | 691 | | |
600 | 692 | | |
601 | | - | |
| 693 | + | |
602 | 694 | | |
603 | 695 | | |
604 | 696 | | |
| |||
614 | 706 | | |
615 | 707 | | |
616 | 708 | | |
617 | | - | |
| 709 | + | |
618 | 710 | | |
619 | 711 | | |
620 | 712 | | |
| |||
703 | 795 | | |
704 | 796 | | |
705 | 797 | | |
706 | | - | |
| 798 | + | |
707 | 799 | | |
708 | 800 | | |
709 | 801 | | |
| |||
1295 | 1387 | | |
1296 | 1388 | | |
1297 | 1389 | | |
1298 | | - | |
| 1390 | + | |
1299 | 1391 | | |
1300 | 1392 | | |
1301 | 1393 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
28 | 28 | | |
29 | 29 | | |
30 | 30 | | |
31 | | - | |
32 | | - | |
33 | | - | |
34 | | - | |
35 | | - | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
36 | 54 | | |
37 | 55 | | |
38 | 56 | | |
| |||
121 | 139 | | |
122 | 140 | | |
123 | 141 | | |
124 | | - | |
| 142 | + | |
| 143 | + | |
| 144 | + | |
| 145 | + | |
| 146 | + | |
| 147 | + | |
| 148 | + | |
| 149 | + | |
| 150 | + | |
| 151 | + | |
125 | 152 | | |
126 | 153 | | |
127 | | - | |
| 154 | + | |
| 155 | + | |
| 156 | + | |
| 157 | + | |
| 158 | + | |
| 159 | + | |
128 | 160 | | |
129 | 161 | | |
130 | 162 | | |
| |||
726 | 758 | | |
727 | 759 | | |
728 | 760 | | |
729 | | - | |
730 | | - | |
| 761 | + | |
| 762 | + | |
731 | 763 | | |
732 | 764 | | |
733 | 765 | | |
| |||
777 | 809 | | |
778 | 810 | | |
779 | 811 | | |
780 | | - | |
| 812 | + | |
| 813 | + | |
| 814 | + | |
| 815 | + | |
| 816 | + | |
| 817 | + | |
| 818 | + | |
| 819 | + | |
781 | 820 | | |
782 | 821 | | |
783 | 822 | | |
| |||
1223 | 1262 | | |
1224 | 1263 | | |
1225 | 1264 | | |
1226 | | - | |
| 1265 | + | |
1227 | 1266 | | |
1228 | 1267 | | |
1229 | 1268 | | |
| |||
0 commit comments