Conversation
…rm-only `TranslationDataSchema` served two bundles at once (per-app `stack.translations` and the platform packages' own code-authored bundles), and that is what made every reading of `settings` wrong. It now names the PER-APP bundle entry — ten groups, `settings` refused by name with a platform-only remedy — and the new `PlatformTranslationDataSchema` / `PlatformTranslationBundleSchema` carry the eleven-group platform face. Ruling batch #132 item 2 letter ② (2026-09-13). Claude-Session: https://claude.ai/code/session_01UDXER3sdqfeVYpEWZs5mZx Co-authored-by: Claude <noreply@anthropic.com>
…n + ADR-0087 semantic entry) Claude-Session: https://claude.ai/code/session_01UDXER3sdqfeVYpEWZs5mZx Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UDXER3sdqfeVYpEWZs5mZx Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UDXER3sdqfeVYpEWZs5mZx Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UDXER3sdqfeVYpEWZs5mZx Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UDXER3sdqfeVYpEWZs5mZx Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 2 package(s): 12 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 2 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 138 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 f8be76094775e5562d65838d15cef48616490850 && git checkout f8be76094775e5562d65838d15cef48616490850
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 70dccce03842a4e43abd91ca38ae6bafa1a28174 9479042a3f31ac54f6072f61300bb7705dc7e669 && git checkout -B drift-repro 70dccce03842a4e43abd91ca38ae6bafa1a28174 && git merge --no-ff 9479042a3f31ac54f6072f61300bb7705dc7e669
node scripts/docs-audit/affected-docs.mjs --json 70dccce03842a4e43abd91ca38ae6bafa1a28174
|
…rm did Rework on the at-tier FAIL. `AppPlugin` loads the app's bundles in its own `start()` (kernel Phase 2); `SettingsServicePlugin` contributes the platform's settings translations from a `kernel:ready` hook (Phase 3); `deepMerge` gives the later source the leaf. So a per-app `settings` entry was a GAP FILLER — it rendered only where the platform bundle carried no string for that key and locale, and lost every key both defined. Dropping it sends those gaps back to the manifest's own English literal, not "the platform's strings again". Nine sites carried the reversed claim: the changeset body, the D2 conversion's docblock and `summary`, the ADR-0087 entry's `reason` and `acceptanceCriteria`, their two generated copies in `migrations/registry.ts` (regenerated, never hand-edited), the step-18 `rationale`, and the parse-time refusal text `PER_APP_SETTINGS_PLATFORM_ONLY` — plus the two schema docblocks that carried it. Text only: no schema, face, ledger, conversion behaviour or level changes. Claude-Session: https://claude.ai/code/session_01UDXER3sdqfeVYpEWZs5mZx Co-authored-by: Claude <noreply@anthropic.com>
`PER_APP_SETTINGS_PLATFORM_ONLY` told a refused author to use "the groups this bundle does declare" and then left out `settingsCommon` — which is precisely the nearest legitimate neighbour for someone who just had `settings` refused, and the one key that makes the boundary legible: the Settings UI SHELL strings stay authorable, only the per-namespace manifest copy leaves. Read off the BUILT `TranslationDataSchema.shape`, the per-app face declares ten. The message now enumerates all ten, in declaration order, and says what the neighbour is. The changeset's own `Clause-②` line moves `no` -> `yes`: four genuinely-new exported symbols trip the mechanical floor. Documentation consistency only — the gate reads the PR body, and the `minor` bumps are unaffected. Claude-Session: https://claude.ai/code/session_01UDXER3sdqfeVYpEWZs5mZx Co-authored-by: Claude <noreply@anthropic.com>
`content/docs/ui/translations.mdx` is a published app-author guide, and its "What you can translate" table told an application to put its settings copy in `settings` — a group this change makes platform-only and refuses by name in a file-authored bundle. Shipping the refusal beside a page that teaches the refused key is the state this PR's own body calls unacceptable. The row now names `globalActions` and `messages` only, and a new row points at `settingsCommon.sourceLabels` — the Settings UI shell strings that DO stay on the per-app face — while saying where the per-namespace manifest copy is translated instead. Two neighbours in the same sections said the opposite and are corrected with it. "Authoring in the product" claimed a `translation` item carries the **same** groups as a file bundle; it carries one more. And "only the groups on this page are accepted ... in a runtime item AND in a file-authored bundle" would have become false the moment `settings` left the page, because the registered item still declares it. Both now state the one asymmetry explicitly. Claude-Session: https://claude.ai/code/session_01UDXER3sdqfeVYpEWZs5mZx Co-authored-by: Claude <noreply@anthropic.com>
…t a literal The semantic migration entry for `translation-per-app-settings-platform-only` enumerated nine of the ten groups the per-app face declares and omitted `settingsCommon` — the nearest legitimate neighbour for an author who has just had `settings` refused. `os migrate meta --from 17` prints this string verbatim to the operator (`packages/cli/src/commands/migrate/meta.ts` renders `surface → replacement`), so the omission reached a user-facing surface. Correcting the literal would leave the construct that produced it. A hand-maintained copy of a schema's key set already drifted here once, through a full review of the surrounding change, so the copy is deleted rather than pinned: `replacement` is now a getter that reads `Object.keys(TranslationDataSchema.shape)`. There is one spelling of the set, and a group added to the per-app face reaches this message the day it is declared, with nothing to remember. Ordered remedy — delete the construct that permits the error before reaching for a check that only reddens it. `Object.keys` on a zod object shape returns the declaration order of the literal it was built from, which is the order the sentence promises; the separator and the shape of the sentence are unchanged, so the rendered paragraph reads as it did. A getter rather than an eager template because importing the registry must not force the lazy translation schema at module load. The registry is generated by concatenating entry literals, and the generator treats a file's imports as scaffolding — so `registry.ts` carries its own hand-written value import outside the generated regions, with a comment saying why. Verified: it survives `gen:migration-registry`, and `system/translation.zod.ts`'s own 26-module closure reaches nothing under `migrations/`, so the edge adds no cycle. The changeset gains one sentence naming `settingsCommon` as unaffected. The declared semver level is untouched — both packages stay `minor`; two prose corrections and one derivation move no published signature. Claude-Session: https://claude.ai/code/session_01UDXER3sdqfeVYpEWZs5mZx Co-authored-by: Claude <noreply@anthropic.com>
Resolves the one hand-written conflict in packages/spec/src/type-alias-convention.pin.test.ts. Both sides added pins off the same base (784) and both took the next free ids Iso877/Iso878: #17551's three api/analytics.zod.ts dataset-selection pins on main, this branch's two system/translation.zod.ts platform-bundle pins. Both intents stack; only the numbering collided. main's landed ids stand, this branch renumbers to Iso880/Iso881, and the count becomes 789 = 786 union 787. Claude-Session: https://claude.ai/code/session_01UDXER3sdqfeVYpEWZs5mZx Co-authored-by: Claude <noreply@anthropic.com>
Discharges the deferral the merge commit recorded. Both files are generated, never hand-merged: `gen:strictness-ledger` for the counts ledger, and `gen:schema` then `gen:docs` for the references index (it renders from the gitignored packages/spec/json-schema/ tree and refuses without the first). Each regenerated file now equals main's version plus this branch's own contribution and nothing else: index.mdx differs from origin/main only by `PlatformTranslationBundle` / `PlatformTranslationData` (System 273 -> 275, total 1534 -> 1536), and the counts ledger only by `system/` 351 -> 352. `check:generated` reports all 15 artifacts up to date. Claude-Session: https://claude.ai/code/session_01UDXER3sdqfeVYpEWZs5mZx Co-authored-by: Claude <noreply@anthropic.com>
Landing path for this PR, measured — three things block it and they have an ORDER
Head
CI is NOT the blocker: 35 check names — 33 ⭐ Why the order matters, and why re-taking the review FIRST would be wasteResolving 1 moves the head. A contract review is a record for a head — so a review taken now is void the moment the conflict is resolved. ⇒ 1 before 2, always, and ⛔ not the other way round. On blocker 2 — stated as a measurement, ⛔ not as an adjudication
⛔ Whether records taken before the move still stand is #19603's question, it is dispatched to another seat, and this seat is a BENEFICIARY of one reading of it — so this seat does not answer it here. What this seat will do instead is strictly additive and makes the question moot for this PR either way: once the conflict is resolved, re-take the record at the constant re-read in that act. A review added is not a verdict overturned, and it costs one round rather than a ruling. The next act, and whose it isThe conflict round is this lane's and is queued behind the seat's current in-flight batch, which is at its limit. ⛔ It is not forgotten and ⛔ it is not waiting on the maintainer: nothing is owed by them until 1 and 2 are done, at which point blocker 3 — the governed-surface authorization — is the only thing left and is theirs alone. Generated by Claude Code |
Resolves the one real conflict: both sides appended a `semantic` entry to step18's `<os-generated semantic:18>` region at the same anchor. Resolved as a union of both entries — `translation-per-app-settings-platform-only` (this branch) and `ui-action-undoable-unfulfillable-refused` (main) — which is the order the generator derives from the entry ids. Verified with `check:migration-registry` (exit 0): the resolved region is byte-identical to what `gen:migration-registry` emits from `src/migrations/entries/`. The two `content/docs/references/*.mdx` artifacts were deferred by the os-regen merge driver and are regenerated in the follow-up commit. Claude-Session: https://claude.ai/code/session_01UDXER3sdqfeVYpEWZs5mZx Co-authored-by: Claude <noreply@anthropic.com>
Discharges the deferral the merge commit took. The os-regen driver merged both `content/docs/references/` artifacts with exit 0 while keeping one side, so the committed merge carried a schema count (1536 / automation 74) that was neither side's. Regenerated from the merged sources with `gen:schema && gen:docs`; the result now carries both sides: - `index.mdx` — 1537 schemas (main's `FlowFunctionLoweredDeclaration` and this branch's `PlatformTranslationBundle` / `PlatformTranslationData`). - `api/protocol.mdx` — main's `enableOnInstall` description and this branch's `TranslationData.settings` row move. `pnpm --filter @objectstack/spec check:generated` reports all 15 artifacts up to date. Claude-Session: https://claude.ai/code/session_01UDXER3sdqfeVYpEWZs5mZx Co-authored-by: Claude <noreply@anthropic.com>
Blockers 1 and 2 are CLEARED. Only blocker 3 remains, and it is the one no seat may clear.
Head is now
CI, read by job conclusions on this head — latest run per check NAME, ⛔ never an aggregate roll-up35 names — 33 Blocker 3, re-measured in this act
1 of 28 paths hits the register: ⚖️ Landing tier H(人合). Two routes, and ⛔ this seat may take neither on its own:
⛔ This seat never approves a governed-surface PR under any account, and ⛔ never merges one. ⭐ One finding from the conflict round, recorded here because it changes how the next one is doneThe dispatch warned that ⇒ ⛔ never read State⛔ PR stays draft. Card #15178 stays Generated by Claude Code |
Fixes #15178
Clause-②: yes
What is ruled, and what landed
Maintainer ruling batch #132 item 2 letter ② (comment 5653315643, 「同意」 2026-09-13), quoted verbatim:
The card's own closing line (「⛔ Not a queue card: zero measured pull today」) is stale and the ruling overrides it. The removal option is struck; this is the SPLIT.
Which name took which face, and why.
TranslationDataSchemakeeps its name and becomes the per-app bundle entry (ten groups); the newPlatformTranslationDataSchema/PlatformTranslationBundleSchema(typesPlatformTranslationData/PlatformTranslationBundle) carry the eleven-group platform face. The ruling's "names per the file's convention" is satisfied by the file's existing habit — a qualified prefix marks the other face, asObjectTranslationDataSchemaalready does — and the direction was chosen on a measurement, not on taste:TranslationDataSchema", and the gate's ledger reason says "removal from the per-app schema" about that same export.TranslationData:stack.translations,defineTranslationBundle, the three examples, the CLI walker and coverage reader, and the headeros i18n extractemits into every scaffolded bundle. Putting the narrowing on a NEW name would have left all of them acceptingsettings, and re-pointing them would have re-typed the nine platform*.generated.tsfiles the same emitter writes.settingsat all.So "every reader that consumes a per-app bundle types against the per-app schema" holds by construction here, and the movement fell on the platform side.
The ruling's "was inert" is falsified — the record says what was measured instead
Ruling item 4 describes a per-app
settingsas inert. Measured onorigin/mainat1ff3a8f210, it was live:AppPlugin.loadTranslations(packages/runtime/src/app-plugin.ts) hands eachstack.translationsbundle entry WHOLE toII18nService.loadTranslations.FileI18nAdapter.loadTranslationsdeep-merges it into the one per-locale tree;getTranslations(locale)serves that tree.kernel:ready—SettingsServicePlugindoes exactly this withsettingsBuiltinTranslations.pickSettingsEntryreadspickData(bundle, locale)?.settings, and the console'suseSettingsLabelscans every namespace carrying asettingsbranch. The tracked liveness ledgerpackages/spec/liveness/translation.jsonrecords that reader with its evidence pointer and says both doors "merge into ONE tree".So an app-authored
settingsdid not sit unread — but nor did it override the platform. The app's bundles load inAppPlugin'sstart()(kernel Phase 2) and the platform's atkernel:ready(Phase 3), anddeepMergegives the later source the leaf, so the platform won every key both defined. What an application actually had was a GAP FILLER on a namespace it does not own: it rendered only where the platform bundle carried no string for that key and locale. That makes the ruling's DIRECTION stronger, not weaker, and it changes only what the record must say: the drop is a visible change only on the screens where the entry was FILLING A GAP, and those fall back to the manifest's own English literal; where the platform already carried the string, nothing changes. The ADR-0087 semantic entry and the changeset both say so in those words rather than reciting the house "pure lossless delete" phrase.Diff
Spec (
packages/spec)src/system/translation.zod.ts—translationDataShape()becomesappTranslationDataShape()(ten groups) and thesettingsgroup moves toplatformSettingsShape().TranslationDataSchema= per-app, strict, with aguidanceprescription for bothsettingsand the singularsetting; thesettingalias is deleted, because an alias prescribing a key the shape now rejects is a suggestion the author cannot take.PlatformTranslationDataSchema= the ten plussettings.TranslationBundleSchemais per-app;PlatformTranslationBundleSchemais new.TranslationItemSchemais UNCHANGED and still declaressettings.src/api/protocol.zod.ts—GetTranslationsResponseSchema.translationsmoves to the platform face. The served document is the merge of every loaded bundle, so it carriessettings; leaving it on the per-app face would have published a declaration the server contradicts.src/system/i18n-resolver.ts—pickDatabecomes generic over the entry type, and the settings resolvers takePlatformTranslationBundle. This WIDENS their parameter (every group is optional, so a per-app bundle is still assignable), so no caller — this repo's or the pinned sibling's — loses a call.src/conversions/registry.ts— new D2translation-per-app-settings-removed(toMajor: 18,retiredFromLoadPath: true), strips the group from per-app bundle ENTRIES only. An entry carryinglocaleis atranslationITEM and is left whole; the candidate value must additionally be a dict whose every key is a declared group, so anobjectsrecord holding an object literally namedsettingsis not mistaken for a bundle.src/migrations/entries/semantic/18.translation-per-app-settings-platform-only.ts— the ADR-0087 semantic entry, plus the regeneratedregistry.tsand the extended step-18 rationale. Regenerated withgen:migration-registry, never hand-merged.src/type-alias-convention.pin.test.ts— the two new aliases are pinned isomorphic (both faces are all-optional, no default or transform anywhere), and the pin count moves 784 → 786 with its receipt.authorable-surface/system.json—system/TranslationData:settingsdeleted DELIBERATELY, the tripwire the strict-delete route owes. The build's own deletion gate then adjudicated it and printed its proof (authorable-surface 的 tombstone 门禁可被手编基线绕过 —— 删掉基线行就删掉了证据(#4638 / #4643 已两次这样过绿) #4650 proof 2): "def not reachable from the 30 metadata-type roots ... an over-collected entry, never parsed against a metadata document." Elevensystem/PlatformTranslationData:*keys arrived in the same run.api-surface/,export-origins/,declaration-map/,json-schema.manifest/,content/docs/references/**, the strictness-ledger counts.The gate (ruling item 3) —
scripts/check-i18n-walk-parity.mjs: thesettingsledger row is gone,LEDGER_CEILING3 → 2, the class-level note and the recorded self-test samples move with it. This is the shrinking direction of a governed, shrink-only ledger, declared in the claim and declared here. The gate's own rule at the ratchet says growth is the reviewed act; slack fails too, which is why the ceiling had to move in the same diff.Platform readers —
packages/services/service-settings/src/translations/{en,es-ES,ja-JP,zh-CN}.tsand theirindex.tsmove toPlatformTranslationData/PlatformTranslationBundle. They are the only bundles in the repo that authorsettings.Published prose —
content/docs/protocol/kernel/i18n-standard.mdxpublished the eleven-group list as "rejected by name at both authoring doors", andskills/objectstack-i18n/SKILL.mdpublished the same list to customer projects. A third published carrier,content/docs/ui/translations.mdx, taughtsettingsas app-translatable in its own table. All three are corrected — an earlier draft of this sentence said "both", before the guide correction landed.docs/qa/platform-checklist/areas/i18n.jsonhad a symbol anchor on the renamed shape function and a clause naming the wrong face.Verification
5283099838, which is the 6th of this branch's 10 commits; four have landed since (b33ea66cb3,d5e6b43977,b1e7984040,8dcd6a42ae). An earlier draft of this line called it "the final commit on this branch", and the whole Verification table, the Tests section and the Reverse verification paragraph hang off it — so as written the body claimed readings that covered the derivation change and the guide correction. They did not.⛔ Nothing below is therefore unmeasured at head; it is re-measured elsewhere, not here. What covers the later commits is the at-tier contract review of head
8dcd6a42ae(record on card #15178), which re-derived the carrier sweep over all 9156 tracked files, rendered the migration TODO live, ran the derivation ablation in memory, and read CI by job conclusion. ✅ One row IS unaffected and re-measured at head: the skills table —skills/objectstack-i18n/SKILL.mdwas last touched at2602ccec10, earlier than5283099838, and every figure in it reproduces at head (494→496 lines, 4713→4752 tokens, ceiling 6338, headroom 1586).check:i18n-walk-parity10 declared group(s), 8 walked, 2 exempted10is the reading that discriminates: the per-app face has ten groups, the platform face eleven. It fails if a declared group has no emitter and no ledger row, if a ledger row is stale, and — the ratchet — if the ceiling has slack. Its--self-testbattery is 43 cases.check:authorable-surface(insidegen:schema)check:generated--fixregenerated the 5 it proved stale, each re-checkedcheck:adr-0087-registration1 declared-breaking changeset(s), each carrying an ADR-0087 disposition0 non-breaking changeset(s) seenbefore the changeset was committed, which is the control leg.check-changeset-no-majorno major bumpLEVEL AXIS: NOT APPLICABLE— there is no PR payload on a local run, so it can fail HERE only on themajoraxis, not on the declaration.check:spec-parsed-alias1455 bare aliases, 786 pinned isomorphic, 669 pairedcheck:type-check-debt4 ledger entr(ies) re-measured, 53 raw tsc errors, none above its recorded numberpnpm lint(eslint . --no-inline-config)Gate families:
scripts/pm/dispatch-gates.mjsderived 139 for this change set;--ranwith exit codes recorded reconciles 139 accounted, 138 run, 0 UNRUN, 1 NOT MEASURED. The one ischeck:dual-build-cjs-loads, which exits 3 (PREREQUISITE NOT MET) without a full workspace build — recorded as NOT MEASURED and left to CI, which builds everything. The reconciliation's own caveat stands: it answers what this card DERIVES against what was RUN, and the artifact-roster families, the wide-population families and the path-scheduled CI jobs are outside that total.Tests (
turbo run test,--concurrency=2):@objectstack/spec508 files / 14,902 tests,@objectstack/lint106 files,@objectstack/service-settings33 files,@objectstack/platform-objects51 files, the three examples 41 files,@objectstack/cliunit tier 222 files / 3,141 tests — all pass.turbo run typecheckover spec, cli, lint, platform-objects, service-settings, service-i18n, runtime and rest: 64 tasks, all pass.Reverse verification (one-shot, not left in the tree). With the fix committed,
settingswas put back on the per-app shape throughscripts/ablation-replace.mjs— the mutation is proved on disk (anchor 1 → 0, blob3a27c26f6a5c→a0b6be68af3e) — and the two refusal pins went RED by name. Restored with--restore: blob back to3a27c26f6a5c, equal to HEAD, andgit diff HEADempty. The predicted direction was "turns red", and that is what was observed.Skills bundle readings (
skills/**is a governed surface)Required because the diff touches a published skill. This is a CORRECTION, not an expansion — the added sentence exists because the old one became false.
skills/objectstack-i18n/SKILL.md, linesskills/objectstack-i18n/SKILL.md, tokensSKILL.mdlinesBefore-tokens were measured by restoring the base file, reading
check-skills-token-ratchet, and restoring with proof (blob equal to HEAD,git diff HEADempty).check-skills-token-ratchetpasses: 34 authored files within their ceilings.skills/**is Tier H on the governed register, so this PR's landing is Tier H on one path hit. It is left as a draft awaiting that record. If the seat would rather land the rest through the queue, the remedy the directive names is to splitskills/objectstack-i18n/SKILL.mdoff into its own PR — but ⛔ not to ship the corrected schema while the published skill still teaches the key the parse now refuses.Declared file-surface deviations
The claim declared the surface as
translation.zod.ts+ siblings, the walk-parity gate + fixtures,i18n-extract.ts+ tests,migrations/entries/semantic/+registry.ts, and.changeset/. Five paths outside it were edited, each forced by the ruling rather than chosen, and none widened silently:packages/spec/src/api/protocol.zod.ts— the served response must type against the platform face or it declares a shape the server contradicts.cursorand computeshasMore#19493's sibling declaration set — specifically it was declared disjoint against [finding] three sibling list doors declarelimit/cursorand never read them, one reportinghasMore: falseas a literal — REBUILD of #19365, which stopped resolving on 2026-09-21 #19543, which enumerates it. One line changes (the import) plus one line in the response schema, plus a docblock. A textual conflict is possible; this PR is not asking to land first.packages/spec/src/system/i18n-resolver.ts—pickSettingsEntryreads.settings; without this the package does not typecheck. The parameter is widened, not narrowed.packages/services/service-settings/src/translations/*(5 files) — the only bundles that authorsettings; type annotations only.packages/spec/src/conversions/registry.ts— the D2 conversion ruling item 4 asks for ("the conversion drops the group and records a note"). The semantic entry alone records the judgment but rewrites nothing.content/docs/protocol/kernel/i18n-standard.mdx,skills/objectstack-i18n/SKILL.md,content/docs/ui/translations.mdx,docs/qa/platform-checklist/areas/i18n.json— published claims this change makes false, plus one symbol anchor the rename broke (check:platform-checklistwent red on it and is green again).content/docs/ui/translations.mdxwas added to this enumeration after the fact: it was edited in commitb1e7984040and an earlier draft of this item listed only three paths, under-declaring the deviation by one.packages/spec/src/migrations/registry.tsis the declared overlap with open PR #19493. Its generated regions were regenerated withscripts/pm/os-regen-merge.sh's generator (gen:migration-registry) and never hand-merged.registry.ts:47carries a value import ofTranslationDataSchema, outside every<os-generated …>region (the first region opens at:1142). It is necessary, not an oversight —build-migration-registry.ts'sparseEntrydeliberately drops imports and carries only the initializer, so an entry that derives its text from a value needs that value in scope in the registry itself. The comment immediately above the import says so. It survives regeneration becauserenderRegistrysplices only between the region markers, andcheck:generatedis the instrument that would fail if that round-trip were unstable.packages/cli/src/utils/i18n-extract.tswas NOT edited — the walker does not move, becausesettingsnever had an emitter.Acceptance notes
translationmetadata-type door is untouched and still acceptssettings. The ruling names the per-app BUNDLE, andpackages/spec/liveness/translation.jsonis that item's ledger, so narrowing the item would have moved a ledger outside this card's surface. It leaves a question worth a decision rather than a silent choice: an app admin authoring atranslationitem through Studio can still writesettings, andauthored-translation-syncmerges the raw stored payload into the same served tree, so the refusal this PR adds at the file door does not reach the metadata door. Raised in the report, not decided here.platform-objects, the five plugins, the other services) keep the narrowerTranslationData/TranslationBundletypes. They are assignable, and re-typing ~20 files that never carrysettingswould be churn with no contract effect. noted, not filed.packages/lint/src/validate-translation-references.tsstill listssettingsamong the groups it deliberately does not judge. The branch is now unreachable for a per-app stack rather than wrong. noted, not filed.维护者速读(草稿)
TranslationDataSchema从此只表示「应用自己写的那一份」,十个分组;新的PlatformTranslationDataSchema是平台那一份,十一个,settings留在它那里。应用再写settings会被按名字拒绝,并告诉作者这是平台专属。settings并不是没人读 —— 它和平台那一份合进同一棵已服务的树。但它不是覆盖者:应用包在 kernel 第 2 阶段加载、平台包在第 3 阶段,合并时叶子归后到的一方,所以两边都定义的键,平台永远赢。应用那份实际是个补缺者:只在平台包对那个键、那个语言没有字符串时才显示。minorchangeset(发射窗口惯例,major会被门禁拒收)。回滚就是回滚这个 PR:没有数据迁移、没有存储改动,os migrate meta的那条转换只在作者主动运行时改源码。skills/**这道 Tier H 的门你本来就得为它开一次。CONTRACT_REVIEW_TIER上重跑;⛔ 结果出来之前本席不做任何落地动作,也不翻 ready。skills/,按规矩属于 Tier H,落地要维护者的那句话;若不想为一条文案更正开这道门,可以把那个技能文件单独拆一个 PR——但⛔ 不能一边发布新契约、一边让已发布技能继续教一个现在会被拒收的键。② 裁决里「was inert」这句与实测不符(它是活的),ADR 条目按实测写,请确认这个改写符合原意。③translation元数据门仍接受settings,那是本卡范围之外的一个口子,见上面的验收注记。Generated by Claude Code