feat(spec)!: the translation item door refuses settings — platform-only at both application doors (#19620) - #19945
Conversation
…stored-row replay Step 2 of the item-door settings retirement: the D2 conversion translation-per-app-settings-removed learns the bare translation item shape (an entry carrying locale, or a top-level declared group for rows written before locale was required) and strips its top-level settings; the ADR-0087 semantic entry and the step-18 rationale are extended to the item door; and authored-translation-sync, which reads sys_metadata itself and merged the raw payload over the shipped bundles, now replays the conversion chain over each stored row before merging it, warning once per row per wiring. Claude-Session: https://claude.ai/code/session_019c3Hi6ZMU1p6m6aA6Bz45d Co-authored-by: Claude <noreply@anthropic.com>
…ly at both doors Step 3 of the item-door settings retirement: TranslationItemSchema takes the per-app face (appTranslationDataShape only), drops the setting -> settings alias, and answers both spellings with its own platform-only guidance, since on the item the group overrode the platform copy rather than filling gaps. The two pins that asserted the item accepts settings now assert the refusal; the liveness row retires by the strict-delete route; the two docs pages and the checklist anchor that said the item still declares it are corrected. Claude-Session: https://claude.ai/code/session_019c3Hi6ZMU1p6m6aA6Bz45d Co-authored-by: Claude <noreply@anthropic.com>
The authorable-surface tripwire line system/TranslationItem:settings is deleted deliberately (the build's check (c) adjudicates it by proof 4, the guidance route); content/docs/references/system/translation.mdx and liveness/state-counts.md are regenerated with gen:docs and gen:liveness-counts. Claude-Session: https://claude.ai/code/session_019c3Hi6ZMU1p6m6aA6Bz45d Co-authored-by: Claude <noreply@anthropic.com>
…level pins and changeset Measured: applyConversionsToStoredItem returned every stored `translation` row untouched, because the manifest-collection maps carry no `translations` spelling, so no rehydration seam ever replayed a translation conversion over a stored row. The pass now maps `translation` (and the legacy plural row spelling) to the `translations` collection the translation conversions walk. Also: the metadata door's 422 INVALID_METADATA refusal of an item carrying settings is pinned (code + status + platform-only prescription, nothing stored, with a control); the stale undrilled-container row translation/settings leaves the shrink-only baseline; the changeset is added and the unreleased sibling changeset's "item unchanged" line is corrected. Claude-Session: https://claude.ai/code/session_019c3Hi6ZMU1p6m6aA6Bz45d Co-authored-by: Claude <noreply@anthropic.com>
…ntries this change extends Claude-Session: https://claude.ai/code/session_019c3Hi6ZMU1p6m6aA6Bz45d Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 2 package(s): 4 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 1 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 141 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin d0ac183a0d4f5324ba1849337d39193c1bed9899 && git checkout d0ac183a0d4f5324ba1849337d39193c1bed9899
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 43460b95aa196410b1acfcaa0bb9af8905066ddc b0f2b0ef060f36c3750cb4d709632f4732214a1d && git checkout -B drift-repro 43460b95aa196410b1acfcaa0bb9af8905066ddc && git merge --no-ff b0f2b0ef060f36c3750cb4d709632f4732214a1d
node scripts/docs-audit/affected-docs.mjs --json 43460b95aa196410b1acfcaa0bb9af8905066ddc
|
Contract reviewServed-tier: 157/157 Isolated at-tier reviewer subagent (every transcript turn served at the tier the constant names), adopted by the Inputs read: issue #19620 body and all 9 comments (ruling 5770445203 letter B, three steps; step ① waived per 5796717943 and 5797554787, which fix the migrate-branch dispatch shape and require step ② to handle stored rows); PR #19945 body, files, diff, comments (one docs-drift bot comment, zero review comments); check-runs at head (35; one failure); refs ① Derived judgments(a) (b) Conversion discriminator is wider than the ruling's parenthetical but cannot misfire on a non-item row or strip anything but the item's own top-level (c) Stored pass now replays conversions over translation rows — exactly three conversions reach them. (d) Core sync replays the chain before merging; warn is loud and specific; fail-safe; scoped. (e) Liveness deletions are forced by the shape change, not a loosened ratchet. (f) ② Semver level
③ Boundary flags
Implemented-by: VERDICT: PASS Generated by Claude Code |
Maintainer confirmation — the DELIBERATE CORRECTION of the pending
|
Fixes #19620
Clause-②: no
What is ruled, and what landed
Ruling-ref
5770445203— batch #210 item 2, letter B, maintainer 「210 同意」:settingsleavesTranslationItemSchematogether with the singular aliassetting: 'settings'; the item door refuses it at parse with the platform-only prescription; thesettingsliveness row retires; the D2 conversiontranslation-per-app-settings-removedlearns the item shape; the ADR-0087 semantic entry is extended. Step ① (the production reading) was waived by the maintainer (records5796717943,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
settingsbefore the runtime reader merges it. It does not, and not for one reason but two. Probes (scratch scripts, not committed), read against this worktree:authored-translation-syncreadssys_metadataitself and deep-merged the RAW stored payload; it never called the conversion chain.applyConversionsToStoredItem(the metadata protocol's stored reads,DatabaseLoader.rowToData,os migrate meta --stored) returned everytranslationrow untouched: the stored pass wraps a row in its stack collection, and the manifest-collection maps carry notranslationsspelling. 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)inpackages/core/src/fallbacks/memory-i18n.tsandpackages/services/service-i18n/src/file-i18n-adapter.ts), so a stored item'ssettingsbeat 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
translationrows (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-removedwalks the bare item shape too: an entry carryinglocale, or one with a declared translation group at its top level (a row written beforelocalewas required, which the sync still reads by its name). Only the item's own top-levelsettingsis stripped; an object literally namedsettingsunderobjectsstays. 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;acceptanceCriteriano longer says the item is unchanged.migrations/registry.tsregenerated withgen:migration-registry; the hand-written step-18 rationale sentence that said the conversion never touches atranslationitem is rewritten.packages/spec/src/conversions/stored.ts—STORED_ONLY_COLLECTIONSmaps a storedtranslation(and the legacy pluraltranslations) row to thetranslationscollection. Kept out of the shared maps, whichcheck:stack-collection-mapsholds to the stack schema.packages/core/src/fallbacks/authored-translation-sync.ts— each row replays the full chain throughapplyConversionsToStoredItembefore the merge (the same policy as every other stored-read seam, PD Add comprehensive test suite for Zod schema validation #12), and each conversion is logged atwarnonce per row per wiring, naming the row, the group ('settings' → '(removed)'), the conversion id, andos migrate meta --stored --apply.settingsrow ofpackages/spec/liveness/translation.jsonis deleted (strict-delete route: the key left the walked shape, a surviving row would be an ORPHAN). Its_noteand the README'stranslationrow say what the deletion does NOT mean: the row'sliveevidence read the SERVED tree, which the platform bundle feeds, so the platform capability is untouched.check:livenessthen namedtranslation/settingsa stale row of the shrink-onlyundrilled-containers.baseline.json; it is deleted.state-counts.mdregenerated.Step ③ — the schema, the alias, the pins
packages/spec/src/system/translation.zod.ts—TranslationItemSchemaspreadsappTranslationDataShape()only (the per-app face, ten groups);setting: 'settings'leaves its alias table;settingsandsettingare answered byITEM_TRANSLATION_KEY_GUIDANCEwith the item's ownITEM_SETTINGS_PLATFORM_ONLY, because the bundle door's sentence ("the platform overwrote it anyway") is false for an item.settingsCommonstays on both faces.packages/spec/authorable-surface/system.json—system/TranslationItem:settingsdeleted deliberately (the check (a) tripwire the strict-delete route owes). The build's check (c) adjudicated it by proof 4: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 refusessettingsandsettingon the item (issue path, keys,PLATFORM group,PlatformTranslationData, no rename suggestion), refuses throughdefineTranslation, with a control.packages/metadata-protocol/src/protocol.invalid-metadata-422-face-inventory.test.ts, section 4): saving atranslationitem carryingsettings/settinganswerscode: 'INVALID_METADATA',status: 422, the issue names the key and saysPLATFORM 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.packages/spec/src/conversions/stored.test.ts(both row spellings dropsettingswith exactly one notice; a canonical row passes through by reference) and the newpackages/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 overcreateMemoryI18n: the platform's邮件投递renders, not the stored override).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.mdxregenerated withgen:docs..changeset/19620-translation-item-settings-platform-only.md—@objectstack/specand@objectstack/coreminor(launch-window convention), BREAKING banner, FROM → TO table, one-line fix, the stored-row behaviour, ADR-0087 dispositionnot-required (already-registered …)because both entries existed and are extended here.PR #19600's acceptance note is superseded
PR #19600 (merged) records "
TranslationItemSchemais UNCHANGED and still declaressettings" and, as its first acceptance note, "Thetranslationmetadata-type door is untouched and still acceptssettings." Both are superseded by this PR: the item door refusessettingswith the platform-only prescription, and rows stored before are converted at every stored seam. The seat carries this sentence to #19600 as a comment..changeset/15178-translation-bundle-split-settings-platform-only.mdis unreleased and its "Unchanged" section said the registeredtranslationitem still declaressettings— 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/maintherefore 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. Noskip-changesetis involved.Declared file-surface deviations
The claim declared
translation.zod.ts+ tests,liveness/translation.json, the conversion + semantic entry + registries, regenerated artefacts,.changeset/, and conditionallyauthored-translation-sync.ts(taken: measured necessary above). Outside it, each forced rather than chosen:packages/spec/src/conversions/stored.ts+stored.test.ts— the stored pass returnedtranslationrows 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).packages/metadata-protocol/src/protocol.invalid-metadata-422-face-inventory.test.ts— the ruling'scode+statuspin lives at the metadata door, not in the schema package.packages/spec/scripts/liveness/undrilled-containers.baseline.json— the shrink-only rowcheck:livenessnamed stale once the key left the shape.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..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 at889861a04—b0f2b0ef0changes only the changeset marker, which no test reads; the 422 file re-run atb0f2b0ef0):@objectstack/spectranslation,i18n-resolver,conversions/*,migrations/*,retired-key-migrate-sentence,alias-integrity,type-alias-convention.pin,metadata-plugin@objectstack/corefallbacks/authored-translation-sync.test.ts(new),fallbacks/fallbacks.test.ts@objectstack/metadata-protocol@objectstack/metadata-protocolb0f2b0ef0@objectstack/service-i18ndistTypecheck:
@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/objectstackderived 116 commands on the actual diff (21 paths vs merge basefdeeea0cc); all 116 run atb0f2b0ef0with the exit code recorded, and--ranreconciles: 116 derived, 116 run, 0 NOT-MEASURED, 0 UNRUN. 115 exit 0; the one exit 1 ischeck-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
.tsfiles,--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}minusNEVER_LINTED) take all 10 of the diff's script files. Invariance:eslint.config.mjsnever enables type-aware linting (its own comment: "noparserOptions.project, no typed@typescript-eslintrules"), so this diff cannot move a verdict on an untouched file. The repo-widepnpm lintis 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 andgit diff HEADempty; the tests readsrc/by relative import, so nodist/was involved). Direction predicted before running:...platformSettingsShape()back on the item shapesettingspins red; the singularsettingpin stays green (its guidance still refuses it)settingsrefusal,defineTranslation);settinggreenSTORED_ONLY_COLLECTIONSconversions,stored,migrationsReverse verification of the rebuilt
.d.ts: a probe file inpackages/core/srctypedconst rejected: TranslationItem = { locale: 'en', settings: … }beside anappscontrol;tsc --noEmit -p packages/coreansweredTS2353 … 'settings' does not exist in type …on thesettingsline only; the probe was removed by a trap and the tree read clean.Acceptance notes
translation-validation-messages-removedandtranslation-component-submit-label-removedalready claimed storedtranslationrows 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.settingsthrough 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 oros migrate meta --stored --applypersists the canonical row.setting(singular) is not converted. An alias only ever suggested a rename in the rejection; the item door never acceptedsetting, so no stored row can carry it.os migrate meta --from 17reports as a semantic TODO, per that command's own docblock — not run here), not restated per conversion in the consumer.62597c588) —TranslationPreview.tsxstill lists asettingsgroup andclientValidation.tsprose counts "19 keys". Neither breaks: the binding importsTranslationItemSchemaitself 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.Generated by Claude Code