Repository navigation
Commit 4ec3987
Fixes #19620
Clause-②: no
## What is ruled, and what landed
Ruling-ref `5770445203` — batch #210 item 2, letter **B**, maintainer
「210 同意」: `settings` leaves `TranslationItemSchema` together with the
singular alias `setting: 'settings'`; the item door refuses it at parse
with the platform-only prescription; the `settings` liveness row
retires; the D2 conversion `translation-per-app-settings-removed` learns
the item shape; the ADR-0087 semantic entry is extended. Step ① (the
production reading) was waived by the maintainer (records `5796717943`,
`5797554787`), so this PR runs step ② and then step ③, and the D2
item-shape conversion takes the **migrate** branch with a loud notice.
Nothing is folded into PR #19600.
## Measured first: stored rows (dispatch assumption 3)
The question was whether teaching the conversion the item shape removes
a stored item's `settings` before the runtime reader merges it. **It
does not, and not for one reason but two.** Probes (scratch scripts, not
committed), read against this worktree:
```text
# at origin/main fdeeea0, spec dist built from it
[A] applyConversionsToStoredItem(translation, item-with-settings): settings survives = true ; notices = []
[B] readAuthoredTranslationLayer(raw row) -> layer[zh-CN].settings = {"mail":{"title":"我的邮件",...}}
# after the conversion learned the item shape (411513f), spec dist rebuilt
SINGULAR_TO_PLURAL.translation = undefined ; PLURAL_TO_SINGULAR.translations = undefined
[stored seam] settings survives = true notices = []
[chain over translations collection] settings survives = false notices = ["translation-per-app-settings-removed"]
```
1. `authored-translation-sync` reads `sys_metadata` itself and
deep-merged the RAW stored payload; it never called the conversion
chain.
2. Even the seams that DO call `applyConversionsToStoredItem` (the
metadata protocol's stored reads, `DatabaseLoader.rowToData`, `os
migrate meta --stored`) returned every `translation` row untouched: the
stored pass wraps a row in its stack collection, and the
manifest-collection maps carry no `translations` spelling. So no seam
had ever replayed ANY translation conversion over a stored row.
And the override is real: both i18n adapters read the runtime-authored
layer OVER the static bundles (`deepMerge(static, authored)` in
`packages/core/src/fallbacks/memory-i18n.ts` and
`packages/services/service-i18n/src/file-i18n-adapter.ts`), so a stored
item's `settings` beat the platform's own copy — the card's confidence
gap 2 is closed, and the core test below pins it end to end.
So the stored-row half lands in two places: the stored pass reaches
`translation` rows (spec, `conversions/stored.ts`), and the runtime sync
becomes a rehydration seam that calls it before merging (core,
`authored-translation-sync.ts` — the claim's conditional surface).
## Step ② — conversion, semantic entry, stored rows
- `packages/spec/src/conversions/registry.ts` —
`translation-per-app-settings-removed` walks the bare item shape too: an
entry carrying `locale`, or one with a declared translation group at its
top level (a row written before `locale` was required, which the sync
still reads by its name). Only the item's own top-level `settings` is
stripped; an object literally named `settings` under `objects` stays.
Surface, summary and docblock say both doors; the fixture gains the item
and a locale-less control.
-
`packages/spec/src/migrations/entries/semantic/18.translation-per-app-settings-platform-only.ts`
— extended to both doors: the item OVERRODE the platform copy (a bundle
entry only filled gaps), so overridden keys go back to the platform
string and filled gaps to the manifest literal; `acceptanceCriteria` no
longer says the item is unchanged. `migrations/registry.ts` regenerated
with `gen:migration-registry`; the hand-written step-18 rationale
sentence that said the conversion never touches a `translation` item is
rewritten.
- `packages/spec/src/conversions/stored.ts` — `STORED_ONLY_COLLECTIONS`
maps a stored `translation` (and the legacy plural `translations`) row
to the `translations` collection. Kept out of the shared maps, which
`check:stack-collection-maps` holds to the stack schema.
- `packages/core/src/fallbacks/authored-translation-sync.ts` — each row
replays the full chain through `applyConversionsToStoredItem` before the
merge (the same policy as every other stored-read seam, PD #12), and
each conversion is logged at `warn` once per row per wiring, naming the
row, the group (`'settings' → '(removed)'`), the conversion id, and `os
migrate meta --stored --apply`.
- Liveness: the `settings` row of
`packages/spec/liveness/translation.json` is deleted (strict-delete
route: the key left the walked shape, a surviving row would be an
ORPHAN). Its `_note` and the README's `translation` row say what the
deletion does NOT mean: the row's `live` evidence read the SERVED tree,
which the platform bundle feeds, so the platform capability is
untouched. `check:liveness` then named `translation/settings` a stale
row of the shrink-only `undrilled-containers.baseline.json`; it is
deleted. `state-counts.md` regenerated.
## Step ③ — the schema, the alias, the pins
- `packages/spec/src/system/translation.zod.ts` —
`TranslationItemSchema` spreads `appTranslationDataShape()` only (the
per-app face, ten groups); `setting: 'settings'` leaves its alias table;
`settings` and `setting` are answered by `ITEM_TRANSLATION_KEY_GUIDANCE`
with the item's own `ITEM_SETTINGS_PLATFORM_ONLY`, because the bundle
door's sentence ("the platform overwrote it anyway") is false for an
item. `settingsCommon` stays on both faces.
- `packages/spec/authorable-surface/system.json` —
`system/TranslationItem:settings` deleted deliberately (the check (a)
tripwire the strict-delete route owes). The build's check (c)
adjudicated it by proof 4:
```text
1 baseline deletion(s) since fdeeea0 carry their own proof (#4650):
- system/TranslationItem:settings — def reachable from the metadata-type roots; writing 'settings' on it is REFUSED as an unrecognized key
and the refusal carries the prescription its `strictObject` declaration owes it ...
```
- Pins flipped (`packages/spec/src/system/translation.test.ts`): "still
accepts every declared group together" now asserts the item refuses
exactly one key, `[['unrecognized_keys', ['settings']]]`; "still accepts
it on the platform face" keeps the platform assertions and drops the
item one; a new block refuses `settings` and `setting` on the item
(issue path, keys, `PLATFORM group`, `PlatformTranslationData`, no
rename suggestion), refuses through `defineTranslation`, with a control.
- Door-level pin
(`packages/metadata-protocol/src/protocol.invalid-metadata-422-face-inventory.test.ts`,
section 4): saving a `translation` item carrying `settings` / `setting`
answers `code: 'INVALID_METADATA'`, `status: 422`, the issue names the
key and says `PLATFORM group`, and nothing is stored; a control saves
the same item without it. It rides that file's already-pinned engine
double, so the engine-double ledger does not move.
- Stored-row pins: `packages/spec/src/conversions/stored.test.ts` (both
row spellings drop `settings` with exactly one notice; a canonical row
passes through by reference) and the new
`packages/core/src/fallbacks/authored-translation-sync.test.ts` (dropped
before the merge, rest of the item kept, one warning naming
row/group/conversion, once per wiring, canonical-row control, and end to
end over `createMemoryI18n`: the platform's `邮件投递` renders, not the
stored override).
- Published prose made false by this change:
`content/docs/ui/translations.mdx` (said the item still declares it),
`content/docs/protocol/kernel/i18n-standard.mdx` (adds the item door),
`docs/qa/platform-checklist/areas/i18n.json` (anchor prose);
`content/docs/references/system/translation.mdx` regenerated with
`gen:docs`.
- `.changeset/19620-translation-item-settings-platform-only.md` —
`@objectstack/spec` and `@objectstack/core` `minor` (launch-window
convention), BREAKING banner, FROM → TO table, one-line fix, the
stored-row behaviour, ADR-0087 disposition `not-required
(already-registered …)` because both entries existed and are extended
here.
## PR #19600's acceptance note is superseded
PR #19600 (merged) records "`TranslationItemSchema` is UNCHANGED and
still declares `settings`" and, as its first acceptance note, "The
`translation` metadata-type door is untouched and still accepts
`settings`." **Both are superseded by this PR**: the item door refuses
`settings` with the platform-only prescription, and rows stored before
are converted at every stored seam. The seat carries this sentence to
#19600 as a comment.
## ⚠️ One red gate by design — a pending release note corrected,
confirmation requested
`.changeset/15178-translation-bundle-split-settings-platform-only.md` is
unreleased and its "Unchanged" section said the registered `translation`
item still declares `settings` — false once this PR lands in the same
release. It is **corrected, not restored** (one sentence: not changed by
THAT entry, superseded in the same release by this PR's changeset).
`node scripts/check-empty-changeset.mjs --base origin/main` therefore
exits 1 in its DELIBERATE CORRECTION class, whose own text says the
remedy is to say so on the PR and get it confirmed. **Please confirm
this correction**; the alternative, restoring the file from base,
republishes the false sentence. No `skip-changeset` is involved.
## Declared file-surface deviations
The claim declared `translation.zod.ts` + tests,
`liveness/translation.json`, the conversion + semantic entry +
registries, regenerated artefacts, `.changeset/`, and conditionally
`authored-translation-sync.ts` (taken: measured necessary above).
Outside it, each forced rather than chosen:
1. `packages/spec/src/conversions/stored.ts` + `stored.test.ts` — the
stored pass returned `translation` rows untouched (measured above);
without it the migrate branch the ruling orders reaches no stored row.
Same defect class, a one-entry map; no open PR on it was checked (not
measured — ordinary concurrency).
2.
`packages/metadata-protocol/src/protocol.invalid-metadata-422-face-inventory.test.ts`
— the ruling's `code` + `status` pin lives at the metadata door, not in
the schema package.
3. `packages/spec/scripts/liveness/undrilled-containers.baseline.json` —
the shrink-only row `check:liveness` named stale once the key left the
shape.
4. `content/docs/ui/translations.mdx`,
`content/docs/protocol/kernel/i18n-standard.mdx`,
`docs/qa/platform-checklist/areas/i18n.json`,
`packages/spec/liveness/README.md` — published claims this change makes
false.
5. `.changeset/15178-…` — the correction above.
## Verification
Commits `411513f09` (step ②), `7ba3d25d9` (step ③), `d94e300e4`
(regenerated artefacts), `889861a04` (stored pass, door pins,
changeset), `b0f2b0ef0` (the changeset's ADR-0087 marker line only).
**Tests** (targeted, through `scripts/pm/os-verify-lock.sh`; run at
`889861a04` — `b0f2b0ef0` changes only the changeset marker, which no
test reads; the 422 file re-run at `b0f2b0ef0`):
| Package | Scope | Result |
| --- | --- | --- |
| `@objectstack/spec` | `translation`, `i18n-resolver`, `conversions/*`,
`migrations/*`, `retired-key-migrate-sentence`, `alias-integrity`,
`type-alias-convention.pin`, `metadata-plugin` | 14 files / 912 tests
pass |
| `@objectstack/core` | `fallbacks/authored-translation-sync.test.ts`
(new), `fallbacks/fallbacks.test.ts` | 2 files / 66 tests pass |
| `@objectstack/metadata-protocol` | whole package (dependency closure
built first) | 188 files pass, 3 skipped / 2676 tests pass, 19 skipped |
| `@objectstack/metadata-protocol` | the 422 file, verbose, at
`b0f2b0ef0` | 11 / 11, the three new cases named |
| `@objectstack/service-i18n` | whole package, against the rebuilt core
`dist` | 5 files / 74 tests pass |
**Typecheck:** `@objectstack/spec` (`tsc --noEmit` + scripts + test
layer), `@objectstack/core`, `@objectstack/metadata-protocol` — all exit
0.
**Gates:** `node scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack` derived 116 commands on the actual diff (21
paths vs merge base `fdeeea0cc`); all 116 run at `b0f2b0ef0` with the
exit code recorded, and `--ran` reconciles: **116 derived, 116 run, 0
NOT-MEASURED, 0 UNRUN**. 115 exit 0; the one exit 1 is
`check-empty-changeset` (above). The whole workspace was built first
(`turbo run build --filter='./packages/*' --filter='./packages/*/*'`, 72
tasks) so the five built-output gates (`check:skill-examples`,
`check:dual-build-cjs-loads`, `check:i18n-walk-parity`,
`check:lean-entry-closure`, `check:type-check-debt`) measured instead of
exiting 3. `check:type-check-debt`: "4 ledger entr(ies) re-measured, 53
raw tsc error(s) total, none above its recorded number."
**Lint, narrowed and proven:** ESLint over the 10 changed `.ts` files,
`--no-inline-config --format json`: 10 files linted, 0 errors, 0
warnings. Population: the config's own globs
(`**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}` minus `NEVER_LINTED`) take all
10 of the diff's script files. Invariance: `eslint.config.mjs` never
enables type-aware linting (its own comment: "no
`parserOptions.project`, no typed `@typescript-eslint` rules"), so this
diff cannot move a verdict on an untouched file. The repo-wide `pnpm
lint` is CI's.
**Ablations** (each through `scripts/ablation-replace.mjs`, after the
fix was committed; anchor hit 1 → 0 and a changed blob proved the
mutation on disk; each restore proved blob == HEAD and `git diff HEAD`
empty; the tests read `src/` by relative import, so no `dist/` was
involved). Direction predicted before running:
| Mutation | Predicted | Observed |
| --- | --- | --- |
| A — `...platformSettingsShape()` back on the item shape | the
`settings` pins red; the singular `setting` pin stays green (its
guidance still refuses it) | 3 red (declared-groups, `settings` refusal,
`defineTranslation`); `setting` green |
| B — stored pass without `STORED_ONLY_COLLECTIONS` | both stored-row
cases red, control green | 2 red, control green |
| C — sync merges without the chain replay | 4 core cases red, canonical
control green | 4 red, 1 green |
| D — conversion's item branch returns the entry | fixture replay +
stored cases red | 4 red across `conversions`, `stored`, `migrations` |
**Reverse verification of the rebuilt `.d.ts`:** a probe file in
`packages/core/src` typed `const rejected: TranslationItem = { locale:
'en', settings: … }` beside an `apps` control; `tsc --noEmit -p
packages/core` answered `TS2353 … 'settings' does not exist in type …`
on the `settings` line only; the probe was removed by a trap and the
tree read clean.
## Acceptance notes
- **Same-class correction riding the stored-pass change.** The docblocks
of `translation-validation-messages-removed` and
`translation-component-submit-label-removed` already claimed stored
`translation` rows replay through them; until this PR none did. Both
strip keys no resolver reads, so the only observable change for them is
a one-time stored-row warning when an old row is read.
- **What an operator sees.** Reading an old row that still carries
`settings` through the metadata API now serves it without the group and
logs the protocol's stored-row warning; the runtime sync logs its own
warning once. A Studio re-save or `os migrate meta --stored --apply`
persists the canonical row.
- **`setting` (singular) is not converted.** An alias only ever
suggested a rename in the rejection; the item door never accepted
`setting`, so no stored row can carry it.
- **The sync's warning is conversion-agnostic.** It names the row, the
dropped group and the conversion; the platform-only reason is carried by
the item-door refusal and by the D3 semantic entry (which `os migrate
meta --from 17` reports as a semantic TODO, per that command's own
docblock — not run here), not restated per conversion in the consumer.
- **Pinned sibling (objectui `62597c588`)** — `TranslationPreview.tsx`
still lists a `settings` group and `clientValidation.ts` prose counts
"19 keys". Neither breaks: the binding imports `TranslationItemSchema`
itself and its parity test compares the schema with itself; the preview
group simply never renders now. Stale prose / dead UI in the sibling;
carrier: none; not filed.
- Size: 21 files, +714 / −173 (887 changed lines), under the 5,000-line
human-merge threshold. No governed surface is touched.
---
_Generated by [Claude
Code](https://claude.ai/code/session_019c3Hi6ZMU1p6m6aA6Bz45d)_
---------
Co-authored-by: Claude <noreply@anthropic.com>
1 parent ae7a35a commit 4ec3987
21 files changed
Lines changed: 714 additions & 173 deletions
File tree
- .changeset
- content/docs
- protocol/kernel
- references/system
- ui
- docs/qa/platform-checklist/areas
- packages
- core/src/fallbacks
- metadata-protocol/src
- spec
- authorable-surface
- liveness
- scripts/liveness
- src
- conversions
- migrations
- entries/semantic
- system
Lines changed: 3 additions & 2 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
66 | 66 | | |
67 | 67 | | |
68 | 68 | | |
69 | | - | |
70 | | - | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
71 | 72 | | |
72 | 73 | | |
73 | 74 | | |
| |||
Lines changed: 71 additions & 0 deletions
| 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 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
406 | 406 | | |
407 | 407 | | |
408 | 408 | | |
409 | | - | |
| 409 | + | |
| 410 | + | |
| 411 | + | |
410 | 412 | | |
411 | 413 | | |
412 | 414 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
506 | 506 | | |
507 | 507 | | |
508 | 508 | | |
509 | | - | |
510 | 509 | | |
511 | 510 | | |
512 | 511 | | |
| |||
604 | 603 | | |
605 | 604 | | |
606 | 605 | | |
607 | | - | |
608 | | - | |
609 | | - | |
610 | | - | |
611 | | - | |
612 | | - | |
613 | | - | |
614 | | - | |
615 | | - | |
616 | | - | |
617 | 606 | | |
618 | 607 | | |
619 | 608 | | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
217 | 217 | | |
218 | 218 | | |
219 | 219 | | |
220 | | - | |
221 | | - | |
222 | | - | |
223 | | - | |
224 | | - | |
| 220 | + | |
| 221 | + | |
| 222 | + | |
| 223 | + | |
| 224 | + | |
| 225 | + | |
| 226 | + | |
225 | 227 | | |
226 | 228 | | |
227 | 229 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
299 | 299 | | |
300 | 300 | | |
301 | 301 | | |
302 | | - | |
| 302 | + | |
303 | 303 | | |
304 | 304 | | |
305 | 305 | | |
| |||
Lines changed: 120 additions & 0 deletions
| 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 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
0 commit comments