Skip to content

feat(spec)!: split the translation bundle type — settings is a platform group, not a per-app one (#15178) - #19600

Draft
os-warren wants to merge 14 commits into
mainfrom
claude/issue-15178-translation-bundle-split
Draft

os-warren wants to merge 14 commits into
mainfrom
claude/issue-15178-translation-bundle-split

Conversation

@os-warren

@os-warren os-warren commented Sep 21, 2026

Copy link
Copy Markdown
Collaborator

Fixes #15178

Clause-②: yes

What is ruled, and what landed

Maintainer ruling batch #132 item 2 letter ② (comment 5653315643, 「同意」 2026-09-13), quoted verbatim:

  1. packages/spec TranslationDataSchema becomes two exports (names per the file's convention): the platform bundle schema (eleven groups, settings included) and the per-app bundle schema (settings absent, strict — an authored settings in a per-app bundle is refused with a remedy saying it is platform-only). Every reader that consumes a per-app bundle types against the per-app schema.
  2. The card's original "removal" disposition is struck: settings is a live platform key (pickSettingsEntry, i18n-resolver.ts:2305; console useSettingsLabel).
  3. check:i18n-walk-parity: the settings exemption disappears with the per-app key; LEDGER_CEILING 3 → 2 in the same PR (the ledger is governed — declared in the claim).
  4. Accept-set narrowing on the per-app bundle: Clause-②: no; ADR-0087 semantic entry — a per-app bundle carrying settings was inert, so the conversion drops the group and records a note; no deprecation window (「创业阶段不渐进」), launch-window convention applies.

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. TranslationDataSchema keeps its name and becomes the per-app bundle entry (ten groups); the new PlatformTranslationDataSchema / PlatformTranslationBundleSchema (types PlatformTranslationData / 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, as ObjectTranslationDataSchema already does — and the direction was chosen on a measurement, not on taste:

  • The card body itself names the narrowing target "the per-app TranslationDataSchema", and the gate's ledger reason says "removal from the per-app schema" about that same export.
  • Every EXISTING per-app author already types against TranslationData: stack.translations, defineTranslationBundle, the three examples, the CLI walker and coverage reader, and the header os i18n extract emits into every scaffolded bundle. Putting the narrowing on a NEW name would have left all of them accepting settings, and re-pointing them would have re-typed the nine platform *.generated.ts files the same emitter writes.
  • Only the genuinely-platform readers had to move, and only one of them authors settings at 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 settings as inert. Measured on origin/main at 1ff3a8f210, it was live:

  • AppPlugin.loadTranslations (packages/runtime/src/app-plugin.ts) hands each stack.translations bundle entry WHOLE to II18nService.loadTranslations.
  • FileI18nAdapter.loadTranslations deep-merges it into the one per-locale tree; getTranslations(locale) serves that tree.
  • Every platform plugin contributes into the SAME tree at kernel:readySettingsServicePlugin does exactly this with settingsBuiltinTranslations.
  • pickSettingsEntry reads pickData(bundle, locale)?.settings, and the console's useSettingsLabel scans every namespace carrying a settings branch. The tracked liveness ledger packages/spec/liveness/translation.json records that reader with its evidence pointer and says both doors "merge into ONE tree".

So an app-authored settings did not sit unread — but nor did it override the platform. The app's bundles load in AppPlugin's start() (kernel Phase 2) and the platform's at kernel:ready (Phase 3), and deepMerge gives 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.tstranslationDataShape() becomes appTranslationDataShape() (ten groups) and the settings group moves to platformSettingsShape(). TranslationDataSchema = per-app, strict, with a guidance prescription for both settings and the singular setting; the setting alias is deleted, because an alias prescribing a key the shape now rejects is a suggestion the author cannot take. PlatformTranslationDataSchema = the ten plus settings. TranslationBundleSchema is per-app; PlatformTranslationBundleSchema is new. TranslationItemSchema is UNCHANGED and still declares settings.
  • src/api/protocol.zod.tsGetTranslationsResponseSchema.translations moves to the platform face. The served document is the merge of every loaded bundle, so it carries settings; leaving it on the per-app face would have published a declaration the server contradicts.
  • src/system/i18n-resolver.tspickData becomes generic over the entry type, and the settings resolvers take PlatformTranslationBundle. 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 D2 translation-per-app-settings-removed (toMajor: 18, retiredFromLoadPath: true), strips the group from per-app bundle ENTRIES only. An entry carrying locale is a translation ITEM and is left whole; the candidate value must additionally be a dict whose every key is a declared group, so an objects record holding an object literally named settings is not mistaken for a bundle.
  • src/migrations/entries/semantic/18.translation-per-app-settings-platform-only.ts — the ADR-0087 semantic entry, plus the regenerated registry.ts and the extended step-18 rationale. Regenerated with gen: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.jsonsystem/TranslationData:settings deleted 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." Eleven system/PlatformTranslationData:* keys arrived in the same run.
  • Regenerated: 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: the settings ledger row is gone, LEDGER_CEILING 3 → 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 readerspackages/services/service-settings/src/translations/{en,es-ES,ja-JP,zh-CN}.ts and their index.ts move to PlatformTranslationData / PlatformTranslationBundle. They are the only bundles in the repo that author settings.

Published prosecontent/docs/protocol/kernel/i18n-standard.mdx published the eleven-group list as "rejected by name at both authoring doors", and skills/objectstack-i18n/SKILL.md published the same list to customer projects. A third published carrier, content/docs/ui/translations.mdx, taught settings as 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.json had a symbol anchor on the renamed shape function and a clause naming the wrong face.

Verification

⚠️ Provenance corrected — this table was NOT read at the final commit. It was read at 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.md was last touched at 2602ccec10, earlier than 5283099838, and every figure in it reproduces at head (494→496 lines, 4713→4752 tokens, ceiling 6338, headroom 1586).

Instrument Reading Which side it can fail on
check:i18n-walk-parity 10 declared group(s), 8 walked, 2 exempted The 10 is 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-test battery is 43 cases.
check:authorable-surface (inside gen:schema) deletion allowed with a printed proof; 11 keys added It refuses ANY authorable key that vanishes without one of four proofs, and it refused this diff on the first attempt — that refusal is the control.
check:generated 15 of 15 artifacts current after --fix regenerated the 5 it proved stale, each re-checked It can fail on a stale artifact in either direction.
check:adr-0087-registration 1 declared-breaking changeset(s), each carrying an ADR-0087 disposition It can fail on a breaking changeset with no marker; it reported 0 non-breaking changeset(s) seen before the changeset was committed, which is the control leg.
check-changeset-no-major no major bump ⚠️ Its clause-② axis printed LEVEL AXIS: NOT APPLICABLE — there is no PR payload on a local run, so it can fail HERE only on the major axis, not on the declaration.
check:spec-parsed-alias 1455 bare aliases, 786 pinned isomorphic, 669 paired It failed first with both new aliases named — that red is the control.
check:type-check-debt 4 ledger entr(ies) re-measured, 53 raw tsc errors, none above its recorded number Re-run after a rebuild; an earlier run exited 3 (PREREQUISITE NOT MET) on a dist older than its sources, which is NOT a reading.
pnpm lint (eslint . --no-inline-config) exit 0, whole repo, no narrowing Ran over the repo's own configured universe, so no narrowing claim is needed.

Gate families: scripts/pm/dispatch-gates.mjs derived 139 for this change set; --ran with exit codes recorded reconciles 139 accounted, 138 run, 0 UNRUN, 1 NOT MEASURED. The one is check: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/spec 508 files / 14,902 tests, @objectstack/lint 106 files, @objectstack/service-settings 33 files, @objectstack/platform-objects 51 files, the three examples 41 files, @objectstack/cli unit tier 222 files / 3,141 tests — all pass. turbo run typecheck over 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, settings was put back on the per-app shape through scripts/ablation-replace.mjs — the mutation is proved on disk (anchor 1 → 0, blob 3a27c26f6a5ca0b6be68af3e) — and the two refusal pins went RED by name. Restored with --restore: blob back to 3a27c26f6a5c, equal to HEAD, and git diff HEAD empty. 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.

Reading Before After Delta
skills/objectstack-i18n/SKILL.md, lines 494 496 +2
skills/objectstack-i18n/SKILL.md, tokens 4713 4752 +39 (ceiling 6338, headroom 1586)
Whole bundle, all SKILL.md lines 6145 6147 +2
Whole bundle, tokens (shipped tree) 140374 140413 +39

Before-tokens were measured by restoring the base file, reading check-skills-token-ratchet, and restoring with proof (blob equal to HEAD, git diff HEAD empty). check-skills-token-ratchet passes: 34 authored files within their ceilings.

⚠️ Landing tier. 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 split skills/objectstack-i18n/SKILL.md off 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:

  1. packages/spec/src/api/protocol.zod.ts — the served response must type against the platform face or it declares a shape the server contradicts. ⚠️ This path is held by open PR feat(automation): GET /automation/:name/runs retires cursor and computes hasMore #19493's sibling declaration set — specifically it was declared disjoint against [finding] three sibling list doors declare limit/cursor and never read them, one reporting hasMore: false as 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.
  2. packages/spec/src/system/i18n-resolver.tspickSettingsEntry reads .settings; without this the package does not typecheck. The parameter is widened, not narrowed.
  3. packages/services/service-settings/src/translations/* (5 files) — the only bundles that author settings; type annotations only.
  4. 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.
  5. 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-checklist went red on it and is green again). ⚠️ content/docs/ui/translations.mdx was added to this enumeration after the fact: it was edited in commit b1e7984040 and an earlier draft of this item listed only three paths, under-declaring the deviation by one.

packages/spec/src/migrations/registry.ts is the declared overlap with open PR #19493. Its generated regions were regenerated with scripts/pm/os-regen-merge.sh's generator (gen:migration-registry) and never hand-merged. ⚠️ One line in that file IS hand-written, and a reviewer of a generated file should be told: registry.ts:47 carries a value import of TranslationDataSchema, outside every <os-generated …> region (the first region opens at :1142). It is necessary, not an oversight — build-migration-registry.ts's parseEntry deliberately 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 because renderRegistry splices only between the region markers, and check:generated is the instrument that would fail if that round-trip were unstable. packages/cli/src/utils/i18n-extract.ts was NOT edited — the walker does not move, because settings never had an emitter.

Acceptance notes

  • The translation metadata-type door is untouched and still accepts settings. The ruling names the per-app BUNDLE, and packages/spec/liveness/translation.json is 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 a translation item through Studio can still write settings, and authored-translation-sync merges 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 bundles that author only shared groups (platform-objects, the five plugins, the other services) keep the narrower TranslationData / TranslationBundle types. They are assignable, and re-typing ~20 files that never carry settings would be churn with no contract effect. noted, not filed.
  • packages/lint/src/validate-translation-references.ts still lists settings among 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 阶段,合并时叶子归后到的一方,所以两边都定义的键,平台永远赢。应用那份实际是个补缺者:只在平台包对那个键、那个语言没有字符串时才显示。
  • 风险与代价(含回滚)。 风险在于:升级后,两边都有的键屏幕上根本不变(平台本来就赢);只有应用包在补空的那些键会变 —— 那里平台压根没有字符串,所以屏幕上会回落到 manifest 自己的英文字面量,而不是「平台自带的字」。⚠️ 一个本地化部署里冒出一串英文,是比「换成平台的措辞」更响亮的一种结果,请按这个来衡量。这是有意的,changeset 与 ADR-0087 条目都写明了,不是静默变化。发布面动了,所以带 minor changeset(发射窗口惯例,major 会被门禁拒收)。回滚就是回滚这个 PR:没有数据迁移、没有存储改动,os migrate meta 的那条转换只在作者主动运行时改源码。
  • 席位意见。 这一轮的方向是 dev 第一手实测出来的,不是复述:证伪器跑真链路,外加两个亮控 —— 一个把加载顺序反过来证明仪器对顺序敏感,一个用平台没翻译的命名空间证明 app 那份真的被加载且在服务。结论从「覆盖平台文案」改成「只填平台没有的空」,所以 ADR 条目按实测写是对的,原裁决的方向不动。本席不建议拆技能文件:拆了等于在窗口期里让已发布技能继续教一个已被拒收的键,而 skills/** 这道 Tier H 的门你本来就得为它开一次。⚠️ 另:本 PR 先前那份契约复审记录已作废(跑在已退役的档位上,台账见 decision(pm): this seat dispatched 5 clause-② reviews at the RETIRED model tier after the maintainer ruling landed — two PRs merged on void verdicts #19603),新的达档复审在 the current CONTRACT_REVIEW_TIER 上重跑;⛔ 结果出来之前本席不做任何落地动作,也不翻 ready。
  • 你要做的。 ① 这个 PR 碰了 skills/,按规矩属于 Tier H,落地要维护者的那句话;若不想为一条文案更正开这道门,可以把那个技能文件单独拆一个 PR——但⛔ 不能一边发布新契约、一边让已发布技能继续教一个现在会被拒收的键。② 裁决里「was inert」这句与实测不符(它是活的),ADR 条目按实测写,请确认这个改写符合原意。③ translation 元数据门仍接受 settings,那是本卡范围之外的一个口子,见上面的验收注记。

Generated by Claude Code

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

github-actions Bot commented Sep 21, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/service-settings, @objectstack/spec, touching 46 documentable anchor(s). ⚠️ 6 changed file(s) yielded no anchor (packages/services/service-settings/src/translations/en.ts, packages/spec/api-surface/system.json, packages/spec/authorable-surface/system.json, …), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

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

  • content/docs/ai/actions-as-tools.mdx (via confirmText (literal, a string literal in platformSettingsShape; a string literal in translationDataShape))
  • content/docs/ai/connect-mcp.mdx (via confirmText (literal, a string literal in platformSettingsShape; a string literal in translationDataShape))
  • 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; the bare tail of client method i18n.getTranslations, bound to GET /i18n/translations/:locale), i18n.getTranslations (sdk, the route ledger binds it to GET /api/v1/i18n/translations/:locale; the route ledger binds it to GET /i18n/translations/:locale, selected by route anchor /translations/:locale))
  • content/docs/api/plugin-endpoints.mdx (via /translations/:locale (route, bridged from symbol GetTranslationsResponseSchema — its route source's handler names it))
  • content/docs/automation/flows.mdx (via successMessage (literal, a string literal in platformSettingsShape; a string literal in translationDataShape))
  • content/docs/getting-started/build-with-claude-code.mdx (via successMessage (literal, a string literal in platformSettingsShape; a string literal in translationDataShape))
  • 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; the bare tail of client method i18n.getTranslations, bound to GET /i18n/translations/:locale), /api/v1/i18n/translations/:locale (route, a path literal in acceptanceCriteria; a path literal in semantic), /translations/:locale (route, bridged from symbol GetTranslationsResponseSchema — its route source's handler names it))
  • content/docs/protocol/kernel/i18n-standard.mdx (via PlatformTranslationData (symbol, a top-level type), TranslationDataSchema (symbol, a top-level const), confirmText (literal, a string literal in platformSettingsShape; a string literal in translationDataShape), globalActions (literal, a string literal in PER_APP_SETTINGS_PLATFORM_ONLY; a string literal in PlatformTranslationDataSchema; a string literal in TranslationDataSchema; a string literal in apply), metadataForms (literal, a string literal in PER_APP_SETTINGS_PLATFORM_ONLY; a string literal in apply), settingsCommon (literal, a string literal in PER_APP_SETTINGS_PLATFORM_ONLY; a string literal in apply), getTranslations (sdk, the bare tail of client method i18n.getTranslations, bound to GET /api/v1/i18n/translations/:locale; the bare tail of client method i18n.getTranslations, bound to GET /i18n/translations/:locale), /translations/:locale (route, bridged from symbol GetTranslationsResponseSchema — its route source's handler names it))
  • content/docs/protocol/objectui/actions.mdx (via confirmText (literal, a string literal in platformSettingsShape; a string literal in translationDataShape), successMessage (literal, a string literal in platformSettingsShape; a string literal in translationDataShape))
  • content/docs/protocol/objectui/record-alert.mdx (via confirmText (literal, a string literal in platformSettingsShape; a string literal in translationDataShape), successMessage (literal, a string literal in platformSettingsShape; a string literal in translationDataShape))
  • content/docs/ui/actions.mdx (via confirmText (literal, a string literal in platformSettingsShape; a string literal in translationDataShape), successMessage (literal, a string literal in platformSettingsShape; a string literal in translationDataShape))
  • content/docs/ui/translations.mdx (via globalActions (literal, a string literal in PER_APP_SETTINGS_PLATFORM_ONLY; a string literal in PlatformTranslationDataSchema; a string literal in TranslationDataSchema; a string literal in apply), metadataForms (literal, a string literal in PER_APP_SETTINGS_PLATFORM_ONLY; a string literal in apply), settingsCommon (literal, a string literal in PER_APP_SETTINGS_PLATFORM_ONLY; a string literal in apply), /translations/:locale (route, bridged from symbol GetTranslationsResponseSchema — its route source's handler names it))

2 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 PER_APP_SETTINGS_PLATFORM_ONLY; a string literal in PlatformTranslationDataSchema; a string literal in TranslationDataSchema; a string literal in apply))
  • content/docs/releases/v17/17-1.mdx (via confirmText (literal, a string literal in platformSettingsShape; a string literal in translationDataShape), successMessage (literal, a string literal in platformSettingsShape; a string literal in translationDataShape))

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
  • 6 changed file(s) yielded no anchor (packages/services/service-settings/src/translations/en.ts, packages/spec/api-surface/system.json, packages/spec/authorable-surface/system.json, …) — pages documenting those are invisible to this run
  • 10 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 — 138 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 70dccce03842a4e43abd91ca38ae6bafa1a28174packageMentionDocs.

Which tree this was computed on

This run read content/docs from f8be76094775e5562d65838d15cef48616490850 — the merge of head 9479042a3f31ac54f6072f61300bb7705dc7e669 into base 70dccce03842a4e43abd91ca38ae6bafa1a28174, 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 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

⚠️ 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 70dccce03842a4e43abd91ca38ae6bafa1a28174 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

…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>

Copy link
Copy Markdown
Collaborator Author

Landing path for this PR, measured — three things block it and they have an ORDER

domain:spec execution seat 2, session session_01UDXER3sdqfeVYpEWZs5mZx, 2026-09-22T11:35Z. ⛔ No label written, ⛔ no ready-flip, ⛔ no enqueue, ⛔ no auto-merge, ⛔ no approving review. This is a reading so the next act is not re-derived.

Head 34b6a25b2cf2ccb02cdddf915a03f75825043c09, draft, 28 files, +929 / −180, 12 commits.

# blocker reading this act
1 merge conflict mergeable: false, mergeable_state: dirty against the base
2 the contract-review record's tier the record on file was taken before the constant moved — see below
3 governed-surface authorization the PR carries needs:contract-review and protocol:system; ⛔ this seat never approves or lands a governed-surface PR, by standing red line

CI is NOT the blocker: 35 check names — 33 success, 2 skipped, ⛔ 0 failure, 0 in_progress. Read by job conclusions, latest run per check NAME, ⛔ never an aggregate roll-up.

⭐ Why the order matters, and why re-taking the review FIRST would be waste

Resolving 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

CONTRACT_REVIEW_TIER moved on origin/main in commit 372931e512 (PR #19684, Fixes #19680, merged 2026-09-22T08:23:11Z) on the maintainer's ruling 5771798588, and RETIRED_TIER_WORDS is now empty. This PR's record predates that move.

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 is

The 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.

⚠️ Merge the base in rather than rebasing, and regenerate any lockfile or generated file with the repo's own tooling, ⛔ never by hand.


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>

Copy link
Copy Markdown
Collaborator Author

Blockers 1 and 2 are CLEARED. Only blocker 3 remains, and it is the one no seat may clear.

domain:spec execution seat 2, session session_01UDXER3sdqfeVYpEWZs5mZx, 2026-09-22T16:55Z. Supersedes the reading in 5775717416 (2026-09-22T11:35Z) on blockers 1 and 2 only; its blocker 3 stands unchanged. ⛔ No label written, ⛔ no ready-flip, ⛔ no enqueue, ⛔ no auto-merge, ⛔ no approving review.

Head is now 9479042a3f31ac54f6072f61300bb7705dc7e669, 28 files, +929 / −180.

# blocker reading this act
1 merge conflict clearedmergeable_state: clean. Resolved by a real merge commit (67e369ecb6, parents 34b6a25b2c + fb7b74691f) plus a regeneration commit
2 the contract-review record's tier cleared — record re-taken on THIS head: comment 5780146227 on card #15178, VERDICT: PASS, Served-tier: on a seat-supplied stamp control of 96/96
3 governed-surface authorization stands — and it is the maintainer's, ⛔ not this seat's

CI, read by job conclusions on this head — latest run per check NAME, ⛔ never an aggregate roll-up

35 names — 33 success, 2 skipped, ⛔ 0 failure, 0 in_progress. All seven required contexts success, each named and read individually: Lint & Repo Gates · TypeScript Type Check · Test Core · Dogfood Regression Gate · Build Core · Temporal Conformance (live PG + MySQL) · Governed Surface Queue Guard. ⇒ genuinely green, ⛔ not filtered-green: nothing required is skipped here.

Blocker 3, re-measured in this act

node scripts/pm/check-governed-merges.mjs --pr 19600exit 3, which is that tool's GOVERNED verdict (⛔ not PREREQUISITE NOT MET — its own table says so, and reading it the other way is a trap this seat has already fallen into once).

1 of 28 paths hits the register: skills/objectstack-i18n/SKILL.md (skills/**, the published skills catalog). One hit governs the whole PR — 「混合 diff 一条命中即整 PR 分叉」 — so the other 27 paths are ⛔ not a mitigating argument. 1,109 changed lines, under the human-merge threshold.

⚖️ Landing tier H(人合). Two routes, and ⛔ this seat may take neither on its own:

  1. The maintainer's hand — flip it ready and merge. ⚠️ It is a draft, and it stays one: the standing red line is 「⛔ 受管面 PR 无授权批准时永不翻 ready、入队或挂 auto-merge」, so ⛔ this seat will not flip it ready to make the merge button appear, even though that is the only thing standing between here and a hand-merge.
  2. An authorized APPROVED review under GOVERNED_APPROVERS on record — then this seat flips ready, enqueues and lands it, and reports the landing shape by parent count.

⛔ 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 done

The dispatch warned that git merge-tree exit 0 is a false green on os-regen paths. On this pair it was ⛔ not hypothetical: the merge commit reverted main's enableOnInstall correction (#19690 / #19691) at content/docs/references/api/protocol.mdx:1913, and the regeneration commit 9479042a3f restored it. ⇒ the regeneration was ⛔ not cosmetic — it repaired a real silent drop, and git diff origin/main...HEAD on that file is now only the intended settings row move.

⇒ ⛔ never read git merge-tree exit 0 as a clean merge on a generated-doc path; diff the merge commit against both parents.

State

⛔ PR stays draft. Card #15178 stays pm:dispatched with its assignee — 在飞卡跟到 MERGED, and this one is waiting on a human step, ⛔ not on more work. Nothing further is owed by this seat until route 1 or route 2 happens; it is carried in the shift report's 等人合项 section and ⛔ the seat does not idle on it.


Generated by Claude Code

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

2 participants