Skip to content

feat(spec)!: the translation item door refuses settings — platform-only at both application doors (#19620) - #19945

Merged
objectstack-fleet[bot] merged 5 commits into
mainfrom
claude/issue-19620-item-door-settings-retire
Sep 24, 2026
Merged

objectstack-fleet[bot] merged 5 commits into
mainfrom
claude/issue-19620-item-door-settings-retire

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

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:

# at origin/main fdeeea0cc9, 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 (411513f09), 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 Add comprehensive test suite for Zod schema validation #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:
1 baseline deletion(s) since fdeeea0cc918 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

…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>
@github-actions github-actions Bot added size/l documentation Improvements or additions to documentation protocol:system tests tooling labels Sep 24, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/core, @objectstack/spec, touching 16 documentable anchor(s). ⚠️ 4 changed file(s) yielded no anchor (packages/spec/authorable-surface/system.json, packages/spec/liveness/README.md, packages/spec/liveness/state-counts.md, …), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

4 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/api/client-sdk.mdx (via getTranslations (sdk, the bare tail of client method i18n.getTranslations, bound to GET /api/v1/i18n/translations/:locale), i18n.getTranslations (sdk, the route ledger binds it to GET /api/v1/i18n/translations/:locale))
  • content/docs/kernel/services-checklist.mdx (via getTranslations (sdk, the bare tail of client method i18n.getTranslations, bound to GET /api/v1/i18n/translations/:locale), /api/v1/i18n/translations/:locale (route, a path literal in acceptanceCriteria; a path literal in semantic))
  • content/docs/protocol/kernel/i18n-standard.mdx (via globalActions (literal, a string literal in TranslationItemSchema), settingsCommon (literal, a string literal in ITEM_SETTINGS_PLATFORM_ONLY), getTranslations (sdk, the bare tail of client method i18n.getTranslations, bound to GET /api/v1/i18n/translations/:locale))
  • content/docs/ui/translations.mdx (via globalActions (literal, a string literal in TranslationItemSchema), settingsCommon (literal, a string literal in ITEM_SETTINGS_PLATFORM_ONLY))

⛔ 1 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/v16.mdx (via globalActions (literal, a string literal in TranslationItemSchema))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • 4 changed file(s) yielded no anchor (packages/spec/authorable-surface/system.json, packages/spec/liveness/README.md, packages/spec/liveness/state-counts.md, …) — pages documenting those are invisible to this run
  • 12 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 60 of 215 client-bound route-ledger rows — the other 155 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 155: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 100 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 141 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 43460b95aa196410b1acfcaa0bb9af8905066ddc → packageMentionDocs.

Which tree this was computed on

This run read content/docs from d0ac183a0d4f5324ba1849337d39193c1bed9899 — the merge of head b0f2b0ef060f36c3750cb4d709632f4732214a1d into base 43460b95aa196410b1acfcaa0bb9af8905066ddc, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# 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

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 43460b95aa196410b1acfcaa0bb9af8905066ddc → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: 157/157 CONTRACT_REVIEW_TIER
Head-sha: b0f2b0ef060f36c3750cb4d709632f4732214a1d

Isolated at-tier reviewer subagent (every transcript turn served at the tier the constant names), adopted by the domain:spec#4 seat 2026-09-24T03:10Z after re-reading ①(a) and ①(c) on the PR head itself (translation.zod.ts:1640 item door without the platform settings spread, which stays on the platform face at :1497; stored.ts:58 / :96 collection map). The reviewer's record follows unedited.

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 origin/main = aeaaa4429, refs/review/pr-19945 = b0f2b0ef0, merge base fdeeea0cc. No gate family re-run.

① Derived judgments

(a) TranslationItemSchema refuses settings and setting — loud, not a strip; settingsCommon untouched. At refs/review/pr-19945:packages/spec/src/system/translation.zod.ts:1640 the item is strictObject with guidance: ITEM_TRANSLATION_KEY_GUIDANCE (:1643), whose table at :639 maps both settings and setting to ITEM_SETTINGS_PLATFORM_ONLY (:620, its own sentence: names the key a PLATFORM group, says the item OVERRODE platform copy, prescribes delete plus PlatformTranslationData plus os migrate meta --from 17). The shape at :1650 spreads ...appTranslationDataShape() only; the ...platformSettingsShape() spread that base had at fdeeea0cc:…translation.zod.ts:1590 is gone. The alias table at :1648 carries no setting; the platform face keeps setting: 'settings' at :1493 (legitimate under 5653315643). Refusal mechanism: strict-object.ts:458 marks unrecognized_keys terminal — a rejection with the guidance text, never a strip. appTranslationDataShape() (:739) declares exactly ten groups with settingsCommon: strictObject( at :1317, unchanged by the diff. Pins: translation.test.ts asserts the full body plus settings yields exactly [['unrecognized_keys', ['settings']]], both spellings carry PLATFORM group and PlatformTranslationData and no rename arrow, defineTranslation throws, control with settingsCommon parses; metadata door: code 'INVALID_METADATA', status 422, nothing stored (protocol.invalid-metadata-422-face-inventory.test.ts section 4). Matches the ruling's "loud, never a silent strip" and step ③'s code+status pin.

(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 settings. conversions/registry.ts:7195 — if ('locale' in entry || Object.keys(entry).some((k) => GROUPS.has(k))) return stripKeys(entry, ['settings'], emit, path) (:7196). stripKeys (:2079) deletes only the named top-level key and returns the same reference when absent. Reach is bounded twice: on the authored path stack.translations is z.array(TranslationBundleSchema) (packages/spec/src/stack.zod.ts:314) and TranslationBundleSchema is z.record(LocaleSchema, TranslationDataSchema) (translation.zod.ts:1506), so entries are locale-keyed maps whose top-level keys are locale codes, never locale or a group name; on the stored path only rows typed translation/translations are wrapped into that collection (stored.ts:58-62, :96). The one theoretical misclassification is a bundle whose locale KEY is literally spelled as a group name (LocaleSchema is an open string, :12), and even then the only effect is removal of a top-level key spelled settings, i.e. a locale named settings — no BCP-47 tag. Fixture pins objects.settings survives on a locale-less legacy row and the ja-JP item loses only its own settings (expectedNotices: 2). Judgment: within the ruling's intent (the item shape), and the widening is what the waiver's "step ② must handle stored rows" requires for rows the sync reads by name (authored-translation-sync.ts:190 region, LOCALE_LIKE fallback). Non-blocking note in ③.

(c) Stored pass now replays conversions over translation rows — exactly three conversions reach them. stored.ts:96 — SINGULAR_TO_PLURAL[singular] ?? STORED_ONLY_COLLECTIONS[singular]; at base SINGULAR_TO_PLURAL had no translation spelling (git grep SINGULAR_TO_PLURAL fdeeea0cc -- packages/spec/src/shared shows the manifest-collection map; stored.test.ts new cases pin both spellings drop settings with one notice and a canonical row passes by reference). Command: git show refs/review/pr-19945:packages/spec/src/conversions/registry.ts | grep -n "mapCollection(stack, 'translations'" → :3156 translation-validation-messages-removed (toMajor 17, strips top-level validationMessages — removed from the shared shape in 17.0.0, no resolver), :7193 translation-per-app-settings-removed (the subject), :7313 translation-component-submit-label-removed (toMajor 18, strips pages.*.components.*.submitLabel on both shapes — no resolver since #9249). walk.ts exports only keyed walkers (mapFlowNodes/mapPages/mapPageComponents/mapDatasources/mapCollection/mapViewPayloads, :149-565) and apply.ts:121 iterates ALL_CONVERSIONS without a generic pass, so no other conversion touches a { translations: [row] } wrapper. book-translations-removed walks books, not affected. Data effect of the two riders: a key nothing reads is dropped from the served/merged shape with one notice; persistence only on explicit --apply or re-save. CLI reach verified: migrateStoredMetadata (protocol.ts:16632) folds by canonicalMetaType (:255 → META_URL_TO_SINGULAR, which lists translation canonical at meta-url-data.generated.ts:78), isNonCanonicalStoredType('translation') is false (:4553), and convertStoredItemDetailed (:4708) skips only flow. So the published remedy os migrate meta --stored --apply is true for translation rows.

(d) Core sync replays the chain before merging; warn is loud and specific; fail-safe; scoped. authored-translation-sync.ts:167 — applyConversionsToStoredItem('translation', data, { onNotice }) runs after the legacy-dialect skip (:155) and before locale resolution and the bookkeeping strip. Warn at :172: names the row (authored translation 'zh-CN'), the retiring protocol, the notice message (built at apply.ts:150-160 with surface, path, 'settings' → '(removed)', and 'translation-per-app-settings-removed'), the consequence ("dropped before the merge and is not served"), and both remedies (Studio re-save, os migrate meta --stored --apply, --from 17). Dedupe set per wiring (:234, passed at :246, key conversionId|rowName at :171). Fail-safe: applyConversions never throws by design (stored.ts module doc; walkers guard with isDict; mapCollection at walk.ts:463-477 skips non-dicts), non-object payloads are already skipped (:141), and the pre-existing chain.catch and per-mutation .catch (:239, :262) hold. Scope: the read is where: { type: 'translation', state: 'active' } (:123) with the plural fallback (:128); nothing else reaches the merge. Pinned end to end in the new authored-translation-sync.test.ts (drop, keep rest, one warn naming row/group/conversion, once per wiring, canonical control, createMemoryI18n renders the platform string).

(e) Liveness deletions are forced by the shape change, not a loosened ratchet. packages/spec/liveness/translation.json row settings (base fdeeea0cc:…translation.json:114) — the key left the walked .strict() shape; the retirement skill's strict-delete route says a surviving row reports ORPHAN (.claude/skills/spec-property-retirement/SKILL.md:107). The _note and README row at head state the platform capability is untouched (true: PlatformTranslationDataSchema:1489 still spreads platformSettingsShape() at :1497). undrilled-containers.baseline.json row translation/settings (base :140) — check-liveness.mts:1417 fails on undrilledStale ("a baseline row whose container … is no longer a container", :710; STALE_UNDRILLED_GUIDANCE at drill.mts:287 says delete it). Removing a row from a shrink-only baseline is a shrink, not a loosening. state-counts.md regenerated (23→22, 932→931). CI check Spec property liveness = success at head.

(f) authorable-surface/system.json line system/TranslationItem:settings (base :1317) deleted — correct. The key is no longer authorable on that def; check:authorable-surface runs in lint.yml job typecheck-source-gates (lint.yml:5543) = check-run Type Check · source gates success at head, so the #4650 deletion proof (proof 4: strictObject refusal with prescription) was adjudicated by CI, not only by the dev's log.

② Semver level

.changeset/19620-translation-item-settings-platform-only.md: @objectstack/spec: minor, @objectstack/core: minor — correct under the launch-window convention (scripts/check-changeset-no-major.mjs header: breaking ships as minor until GA; ruling: "Changeset minor"). Core bump is warranted: a published runtime behaviour changed (sync replays the chain). Carriers present: BREAKING banner line 6, Clause-②: no line 8 (the ruling's spelling; AGENTS.md step 3 permits no with no arm), FROM → TO table with three rows and the one-line fix, stored-row behaviour, the on-screen effect. Marker: not-required (already-registered translation-per-app-settings-removed, translation-per-app-settings-platform-only) … — the gate's parse regex (check-adr-0087-registration.mjs:1938) splits ids on comma/space; already-registered requires every id to resolve at HEAD and exist at the merge base (:143), and registered would be REFUSED because none is new (:137, battery R5/"none of those ids is NEW"). Both ids exist at base: git show fdeeea0cc:packages/spec/src/conversions/registry.ts | grep -n translation-per-app-settings-removed → 7157; git show fdeeea0cc:packages/spec/src/migrations/registry.ts | grep -n translation-per-app-settings → 5311, 12744; entry file 18.translation-per-app-settings-platform-only.ts present in the base tree. Spelling is right. The step-18 ledger text rewrite cannot drift spec-changes.json/protocol-upgrade-guide.md: both project only up to protocol 17 (spec-changes.json protocolVersion 17.0.0, aggregate 16→17; grep for any step-18 id returns nothing), and check:spec-changes/check:upgrade-guide/check:migration-registry ran green in Type Check · source gates and Lint & Repo Gates. "from this major" mirrors the 15178 predecessor (origin/main:.changeset/15178-…md:64).

③ Boundary flags

  • Deliberate correction of unreleased .changeset/15178-…md — modified sentence: "is not changed by THIS entry … (Superseded in the same release: [Decision] TranslationItemSchema still declares settings — the item door can override platform settings copy, and it is the stronger of the two doors the batch #132 ruling only half closed #19620 narrows the item door too; see its own changeset.) GET /api/v1/i18n/translations/:locale still declares it … typed against the platform face". True at head: GetTranslationsResponseSchema at protocol.zod.ts:3177-3179 uses PlatformTranslationDataSchema; both changesets sit in .changeset/ at head. The red is exactly that class: check-run Check Changeset step 12 failed with one annotation naming only that file and quoting the finding: random changeset filenames collide silently across parallel agents — a round overwrote a sibling PR's minor changeset and every gate stayed green #17712 DELIBERATE CORRECTION remedy ("say so on the PR and get it confirmed"); rule 1 (empty frontmatter) passes for the new file. The PR body says so and asks for confirmation. Non-blocking, but load-bearing for the merge: steps 13–15 (ADR-0087 disposition, allow-major label, no-major guard) were SKIPPED downstream of step 12, so the marker and bump level were not machine-verified in CI at this head — my reading in ② stands in for that until a human confirms and the job re-runs.
  • Files outside packages/spec, each with a reason in the diff: packages/core/src/fallbacks/authored-translation-sync.ts + test — the claim's declared conditional surface, measured necessary (raw-row merge, the waiver's explicit requirement); packages/metadata-protocol/src/protocol.invalid-metadata-422-face-inventory.test.ts — ruling step ③'s code + status pin lives at the door, rides the file's existing engine double; content/docs/ui/translations.mdx, content/docs/protocol/kernel/i18n-standard.mdx — published claims made false (the old text said the item still declares it); content/docs/references/system/translation.mdx — gen:docs output; docs/qa/platform-checklist/areas/i18n.json — anchor prose corrected, true at head; packages/spec/liveness/README.md — the row deletion's record. Release-owned content/docs/releases/v16.mdx (drift bot) untouched. .changeset/15178 above.
  • Pins: every new/flipped assertion is weight-bearing (ablations A–D in the PR body map one to one to settings shape, STORED_ONLY_COLLECTIONS, chain replay, item branch); the "not a rename arrow" assertion pins the alias removal; the by-reference control pins idempotence. No non-weight-bearing pin found.
  • Non-blocking: the sync's warn is conversion-agnostic (the platform-only reason rides the item-door refusal and the D3 entry via --from 17, not the consumer) — consistent with PD Add comprehensive test suite for Zod schema validation #12 and the ruling's "loud notice".
  • Non-blocking: rows stored under the plural type translations are in isNonCanonicalStoredType's reported-not-converted class for the CLI --stored pass (pre-existing migrateStoredMetadata reports a row stored under a non-canonical type as canonical — the stored migration has no finish line for the second-namespace residue #8957 behaviour); the runtime sync still converts them via its fallback read and STORED_ONLY_COLLECTIONS.translations.
  • Non-blocking: Console Pin Gate was skipped at head (ci.yml console filter at :70-77 lists only the pin and console scripts, not packages/spec/**), so the PR's claim that the pinned objectui sibling (TranslationPreview.tsx settings group, clientValidation.ts "19 keys" prose) breaks nothing is unmeasured by CI on this PR; it is prose/dead-UI per the PR and outside this review's read set.
  • Non-blocking: changeset FROM → TO row 3 says "move the copy to the PLATFORM bundle (PlatformTranslationData), or delete it" — readable only by a platform-package author; the paragraph beneath ("not application-authorable … correct it in settingsBuiltinTranslations") carries the correct prescription.

Implemented-by: claude/issue-19620-item-door-settings-retire
Reviewed-by: session_019c3Hi6ZMU1p6m6aA6Bz45d

VERDICT: PASS


Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Maintainer confirmation — the DELIBERATE CORRECTION of the pending .changeset/15178-translation-bundle-split-settings-platform-only.md, 2026-09-24T14:39Z

domain:spec seat 4 (session_019c3Hi6ZMU1p6m6aA6Bz45d), carrying the maintainer's sentence to the PR.

Provenance.

The red it answers, recorded here as the red-by-design route requires:

  • The gate: Check Changeset. Its check-empty-changeset.mjs step fails in the DELIBERATE CORRECTION class on the one sentence this PR corrects in the unreleased 15178 changeset. That sentence said TranslationItemSchema still declares settings, which this PR makes false in the same release.
  • The script's own text (scripts/check-empty-changeset.mjs:127, origin/main): 「DELIBERATE CORRECTION -> do NOT restore it; get it confirmed on the PR」. The confirmation is above.
  • The gate does not run in the merge group: .github/workflows/pr-automation.yml triggers on pull_request only (0 merge_group hits at origin/main). Check Changeset is not one of the queue's seven required contexts. The red therefore stays advisory and blocks nothing.
  • The at-tier record 5806788396 (PASS on this head) confirmed the red is exactly that class and nothing else, and read the ADR-0087 marker and the bump level itself (steps 13–15 were skipped downstream of step 12).
  • ⛔ No skip-changeset label, as the maintainer's earlier route for this class requires.

Landing, in the same act: head b0f2b0ef06 is unchanged since the PASS; every other check-run is success or skipped by design; mergeable: true; not a governed surface; 887 lines. ⇒ ready + auto-merge.


Generated by Claude Code

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review September 24, 2026 14:40
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Sep 24, 2026
Merged via the queue into main with commit 4ec3987 Sep 24, 2026
36 of 37 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-19620-item-door-settings-retire branch September 24, 2026 15:03
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation protocol:system size/l tests tooling

Projects

None yet

1 participant