Skip to content

feat(spec)!: a locale tag written in another case is corrected to its BCP-47 case - #29833

Merged
objectstack-fleet[bot] merged 6 commits into
mainfrom
claude/project-thread-bo9b0g
Nov 3, 2026
Merged

objectstack-fleet[bot] merged 6 commits into
mainfrom
claude/project-thread-bo9b0g

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Nov 3, 2026 •

Copy link
Copy Markdown
Contributor

domain:spec#2 · os-support-ai · session_01WGufzhJG3tKnksHVa1krBz

Requested by Yibo Wang · project thread
Fixes #28993
Supersedes #29190 (same commits, resumed on a new branch)
Ruling-ref: 6595806433

Clause-②: no (narrowing)
Metadata-protocol: narrowed
Skills-docs: updated here — content/docs/protocol/kernel/i18n-standard.mdx (the "Case is not judged: the resolvers fold it" sentence now says case is corrected on parse, a stored row's locale is folded by the conversion translation-locale-tag-case-folded, and a bundle with two keys that fold to one tag is refused). skills/objectstack-i18n/SKILL.md already spells every tag in its canonical case and teaches no case rule, so no skill text or eval moves.

维护者速读

  • 改了什么:区域标签(locale)大小写写得不标准时,解析会改成 BCP-47 标准写法(zh-cn → zh-CN,zh-hans-cn → zh-Hans-CN),只改大小写,不改别的;库里已经按 zh-cn 存下的翻译,读出时由一个新的存量转换(translation-locale-tag-case-folded)按标准写法归位。同一个翻译包里同时写了 zh-cn 和 zh-CN 两个键,会被拒绝并点名两个键。
  • 为什么改:翻译以 zh-cn 保存时接口回 200,但 zh-CN 会话永远看不到这些翻译(i18n: a translation saved with locale 'zh-cn' answers 200 but is never merged for a zh-CN session — #25822 left case unjudged on a runtime case-fold that does not happen #28993),因为语言解析先精确匹配,内置的 zh-CN 包总是先命中。现在解析层只留一种写法,每个读者看到同一个标签。
  • 风险与回滚:这是一处破坏性收窄(BREAKING),维护者已按 A 裁定(拒绝,裁决 6595806433):同一个翻译包里只差大小写的两个键(如 zh-cn 和 zh-CN)以前能通过,现在会被拒绝并点名两个键,仓内命中 0 处。大小写更正本身是放宽,其它原来能通过的输入照样通过,JSON Schema 不变。还有两处按原写法精确比较的地方(邮件模板的语言下限告警、通知模板的语言梯):配置里写成 zh-cn 的默认或支持语言现在读作 zh-CN,存量模板行若仍标 zh-cn 就对不上,需要把模板行改成标准写法,行侧归位随 server 车道的入口卡落地。备好的「合并」替代方案不采用。保存入口仍原样存 zh-cn(入口属 server 车道,另起一张卡),但读出已归位,不影响本卡效果。回滚即撤掉本 PR。
  • 先看:packages/spec/src/system/translation.zod.ts(canonicalLocaleCase 与两个翻译包 schema 上的 superRefine / overwrite);packages/spec/src/conversions/registry.ts 新增的 translation-locale-tag-case-folded(存量行大小写归位,排在 spec: a translation item accepts any locale string ("not a locale!!" saves 200, valid) — LocaleSchema is a bare z.string() #25822 的 translation-locale-tag-canonicalized 之后);packages/spec/src/system/translation.test.ts 新增的一组测试。
  • 两分钟验证:pnpm --filter @objectstack/spec exec vitest run src/system/translation.test.ts,应全部通过,其中包含新增的「LocaleSchema corrects a tag to its BCP-47 case (i18n: a translation saved with locale 'zh-cn' answers 200 but is never merged for a zh-CN session — #25822 left case unjudged on a runtime case-fold that does not happen #28993)」一组。
  • 需要你做的:无,席位落地。

What changes

  • LocaleSchema corrects case (packages/spec/src/system/translation.zod.ts). A tag that passes LOCALE_TAG_SHAPE is returned in its BCP-47 case (RFC 5646 §2.1.1): the language subtag lower case, a 4-letter script title case, a 2-letter region upper case, any other subtag lower case, and every subtag after a singleton lower case. The fold is .overwrite(canonicalLocaleCase), so z.input and z.output are both string, Locale stays z.input, and the published JSON Schema does not move (check:generated regenerated nothing). A canonical tag is returned unchanged, and nothing that parsed before is refused except the bundle case below.
    • Case only, not Intl.getCanonicalLocales. Measured on Node v22.23.4: Intl.getCanonicalLocales rewrites more than case (iw → he, in → id, tl → fil, cmn → zh, mo → ro, art-lojban → jbo, en-us-POSIX → en-US-u-va-posix) and throws on tags LOCALE_TAG_SHAPE accepts (zh-Hans-Hans, sgn-BE-FR, en-GB-oed, ar-aao, en-a-b). Replacing a legacy subtag would move a translation to a locale its author never named, and the answer would depend on the runtime's ICU data.
    • Readers whose output changes: a translation item's locale; TranslationBundle / PlatformTranslationBundle keys; i18n.defaultLocale, fallbackLocale, supportedLocales; a seed dataset's locale scope (data/seed.zod.ts) and the seed loader's locale (data/seed-loader.zod.ts). EmailTemplateDefinitionSchema.locale does not use LocaleSchema and is untouched (spec: the email template locale docs say the resolver folds no language subtag and retries only en-US, which #28880 makes wrong #28896 / docs(spec): the email template locale docs describe the ladder #28880 landed #29014 unaffected); PackageLocaleSchema (marketplace) is a separate schema and is untouched.
  • Bundle keys fold, and two keys that fold to one tag are refused (the narrowing, BREAKING; see Acceptance notes). Both bundle schemas are now z.record(LocaleTagKeySchema, …).superRefine(refuseLocaleKeysFoldingTogether).overwrite(foldLocaleKeys). The key schema is a shape-only twin of LocaleSchema (same regex, message and description, no fold): a folding key schema would let z.record keep the later of two colliding keys and drop the other with no issue. The refinement runs before the fold and refuses { 'zh-cn': …, 'zh-CN': … } with a custom issue at the second key, naming both keys, the tag and the release (Refused since @objectstack/spec 18). The fold then rebuilds the record with canonical keys, in order, and returns the input itself when every key is already canonical.
  • Stored rows read back canonical, through a D2 entry of their own (packages/spec/src/conversions/registry.ts). The new ADR-0087 D2 conversion translation-locale-tag-case-folded (toMajor: 18, retiredFromLoadPath: true, retiredAfter: '18.2.0', the same surface as spec: a translation item accepts any locale string ("not a locale!!" saves 200, valid) — LocaleSchema is a bare z.string() #25822's entry) folds a tag in another case to the LocaleSchema parse: an item's locale, i18n.defaultLocale / fallbackLocale / supportedLocales, data[].locale[] and bundle keys, one notice each. It folds tag-shaped values only and leaves the rest to the entry before it. A bundle key that folds onto a key already holding different strings is left as stored and reported as a TODO (naming the release); an identical twin is merged by renameKey. Its order is 76.5, right after spec: a translation item accepts any locale string ("not a locale!!" saves 200, valid) — LocaleSchema is a bare z.string() #25822's translation-locale-tag-canonicalized (76), so an underscore spelling is made a tag first and folded second (zh_cn → zh-cn → zh-CN).
    • spec: a translation item accepts any locale string ("not a locale!!" saves 200, valid) — LocaleSchema is a bare z.string() #25822's entry keeps its own stamp. translation-locale-tag-canonicalized stays retiredAfter: '18.1.0' with its code unchanged: its docblock now says case is kept as written there and points at the case entry, retired a release later, because the premise that every resolver folds case was measured false. Its fixture moves only to stay disjoint (see Forced test edits).
    • Why stored rows need it. The save door (saveMetaItem) stores the authored body, so a zh-cn save stays zh-cn at rest; core's readAuthoredTranslationLayer replays the full conversion chain over each stored row before keying it by data.locale, so the row reaches zh-CN sessions from the next sync on, with nothing to re-save. An artifact built by @objectstack/spec 18.2.0 or earlier is folded the same way inside its declared-floor window.
  • Exact matchers that read a folded locale against a row tagged as written. Two consumers compare a folded i18n locale with a row whose locale is not folded: the email-template floor warning (warnEmailTemplateLocaleFloor in stack.zod.ts) compares i18n.supportedLocales with emailTemplates[].locale, and the messaging template ladder compares localeCandidates(i18n.getDefaultLocale()) with sys_notification_template.locale. A row tagged zh-cn no longer matches a supported or default locale authored as zh-cn, which now reads zh-CN. The D3 entry's replacement and acceptance criteria name both: the consumer folds case or the row is re-tagged, and the row-side fold lands with the server sibling card (the save door storing the parsed locale). Not changed here.
  • Upgrade path: new ADR-0087 D3 semantic entry packages/spec/src/migrations/entries/semantic/18.translation-locale-case-canonicalized.ts, conversionIds: ['translation-locale-tag-case-folded']. The sibling entry 18.translation-locale-non-tag-refused.ts said case is not refused because the resolvers fold it; it now says the parse corrects it.
  • Docs: content/docs/protocol/kernel/i18n-standard.mdx, one paragraph (see Skills-docs:). This file is outside the claim's listed surface: a bounded in-place fix of a sentence this diff makes false.
  • Dropped-refinements ledger (packages/spec/dropped-refinements.baseline.json): +6 sites: 1012 → 1018 on the merged base b3d14150 (983 → 989 on the original base 6b3ba82c; see Verification for the merge's one conflict, in this file). The bundle superRefine has no JSON Schema form, so it lands at the root of system/TranslationBundle and system/PlatformTranslationBundle, and at manifest.translations.element in the four api/*InstalledPackage* responses that embed a bundle. Entries are the ones the spec build's gate printed.
  • Changeset: .changeset/28993-locale-case-canonicalized.md, @objectstack/spec: minor, titled feat(spec)!:. It carries the two declaration lines above, a BREAKING banner for the refusal with its migration (FROM a bundle holding both spellings, { 'zh-cn': A, 'zh-CN': B }, TO one canonical key holding both sets of strings, merged by hand), and one ADR-0087 disposition, registered translation-locale-case-canonicalized (the D3 entry is new in this diff and prescribes that merge). Its stored-rows bullet names the new conversion, and a bullet names the two exact matchers.
  • Not changed, on purpose: core memory-i18n.ts#resolveLocale (triage 6570179836: no third runtime case-fold); the save door in packages/metadata-protocol (domain:server, handed off as a sibling card); the two exact matchers above.

Forced test edits

  • packages/spec/src/conversions/registry.ts, the fixture of spec: a translation item accepts any locale string ("not a locale!!" saves 200, valid) — LocaleSchema is a bare z.string() #25822's translation-locale-tag-canonicalized: its stored item locale: 'pt-br' is now pt-BR on both sides (before and after), expectedNotices unchanged at 7. The fixture runner replays the whole chain, so a pt-br there would now be folded by the case entry and give one notice that is not this entry's; fixtures stay disjoint. The case belongs to the new entry's fixture.
  • packages/spec/src/shared/retired-key-removal-version.pin.test.ts: translation-locale-tag-case-folded is retired after 18.2.0, the newest published stable release, so the census pin requires a JOIN row with the prescriptions an author meets. A lone tag is corrected, not refused, so the row reads the collision two keys of one bundle make once folded: the bundle refusal at the parse, and the conversion's TODO for a stored pair (a new todo row kind, for a data-at-rest prescription that has no parse refusal to read). Both messages now name @objectstack/spec 18, which the row derives from the stamp.
  • packages/spec/src/system/translation.test.ts: the existing accepts the tag %j (case is not judged) row is retitled (case is corrected, never refused). Its assertion is unchanged; the old title stated the rule this PR reverses.

Tests

packages/spec/src/system/translation.test.ts, new describe LocaleSchema corrects a tag to its BCP-47 case (#28993):

  • Fold table: zh-cn, ZH-CN → zh-CN; zh-hans-cn, ZH-HANS-cn → zh-Hans-CN; EN → en; en-us → en-US; yue-hk → yue-HK; sr-LATN-rs → sr-Latn-RS; es-419, de-1996 unchanged; en-US-u-CA-Gregory → en-US-u-ca-gregory; en-x-US → en-x-us.
  • A canonical tag is returned unchanged (6 tags).
  • Case only: iw stays iw, TL → tl, cmn-cn → cmn-CN, and zh-hans-hans → zh-Hans-Hans (a tag Intl.getCanonicalLocales throws on).
  • Every reader sees the canonical spelling: a translation item, defineTranslation, TranslationConfig (defaultLocale, fallbackLocale, supportedLocales), SeedSchema.locale, SeedLoaderConfig.locale.
  • Bundle keys are folded, in authored order, values untouched: TranslationBundleSchema, defineTranslationBundle, PlatformTranslationBundleSchema.
  • Refusal: both bundle schemas refuse zh-cn beside zh-CN with exactly [['custom', 'zh-CN']] (code, path), and the message names both keys.
  • CONTROL accepts: distinct tags in one bundle (zh-CN, zh-TW, zh-hans-cn); zh-cn in one bundle and zh-CN in another, each parsed on its own (two bundles of stack.translations are separate records the runtime merges per locale).
  • Stored-row seam: applyConversionsToStoredItem('translation', { locale: 'zh-cn', … }) reads back zh-CN; a canonical row reads back equal; an underscored zh_cn row reads back zh-CN (the order: spec: a translation item accepts any locale string ("not a locale!!" saves 200, valid) — LocaleSchema is a bare z.string() #25822's entry makes it a tag, the case entry folds it).
  • Case entry at rest: retiredAfter is 18.2.0; a bundle with zh-cn beside a different zh-CN is left as stored with one TODO at translations[0].zh-cn; an identical ja-jp / ja-JP twin is merged with one notice.
  • The case entry's own fixture (run by the conversion fixture suite over the whole chain): pt-br → pt-BR, bundle keys ko-kr → ko-KR and IT → it, i18n en-us → en-US and zh-hans-cn → zh-Hans-CN, data[].locale zh-cn → zh-CN; 7 notices.

Acceptance notes

  • The narrowing beside the widening. A bundle holding two keys that fold to one tag parses on main today (both keys kept). After this PR it is refused, naming both keys. Measured on 6b3ba82c over every TS/TSX/JS/MJS/CJS/JSON/YAML file in the repo: 0 such pairs, and 0 locale-keyed bundle keys in a non-canonical case outside tests and the conversion fixtures. Under AGENTS.md's level precedence (a value refused is narrowed, narrowed over semantic) the seat amended the claim (6573207423) to Clause-②: no (narrowing) / Metadata-protocol: narrowed, and this body and the changeset carry those lines. The maintainer ruled A on the seat's decision card (6595806433, 2026-11-03): refuse, naming both keys. The prepared merge swap (B, a one-commit patch on 263f6c89) is not taken.
  • The save door. Measured with a held-mutation probe: saveMetaItem (packages/metadata-protocol/src/protocol.ts) runs schema.safeParse(item) and stores the request body verbatim for every type except view, plus four named grafts, so a schema transform never reaches storage. Storing the parsed locale there is a domain:server slice; the seat files it. Until it lands, a lowercase save is raw at rest and the case entry keeps folding it on every read.
  • packages/lint/src/lint-liveness-properties.ts and its test describe a bundle as z.record(LocaleSchema, TranslationDataSchema) in comments. The key schema is now a shape-only twin of LocaleSchema; the comments' point (keys are locale codes, groups live one level down) still holds. Not changed here.

Verification

Resumed head 62f265f0 (this PR)

This PR is #29190's five commits unchanged (5397e120 … 263f6c89), plus one merge of origin/main (62f265f0, main at b3d14150). The merge conflicted in one file, packages/spec/dropped-refinements.baseline.json, on the count line and on two site lists main had re-sorted; it was resolved by keeping both sides: main's sites plus this PR's six translations sites, and the counts main's 250 schemas / 1012 sites plus this PR's 6 = 1018 (the diff of that file against main is the same +6 sites as #29190's diff against its own base). The locale fold, the collision refusal, the D3 entry and the conversion registry merged without a conflict: for the other eight files, this PR's added and removed lines against main are identical to #29190's against its base (compared per file by hash). bash scripts/pm/os-regen-merge.sh then read the recorded pre-merge base and reported no generated path to restore. No behaviour change. Before opening, origin/main was 23 commits ahead with no conflict (git merge-tree --write-tree); the only family-bearing file it moved was a svelte devDependency bump in package.json. Every run below is on 62f265f0; heavy runs went through os-verify-lock.sh on a shared box.

Build, spec suites, generated artifacts
  • turbo run build --filter='!@objectstack/docs' --concurrency=2: Tasks 117 successful, 117 total.
  • pnpm --filter @objectstack/spec check:generated --fix: "All 14 generated artifacts are up to date", nothing regenerated.
  • pnpm --filter @objectstack/spec test: Test Files 804 passed (804); Tests 23909 passed, 1 todo. Exit 0.
  • pnpm --filter @objectstack/spec typecheck: exit 0.
  • pnpm --filter @objectstack/spec test:repo: Test Files 89 passed (89); Tests 1522 passed. Exit 0.
Dogfood battery (on 62f265f)
  • pnpm --filter @objectstack/dogfood test after the full build above, under the lock, no OS_TEST_SHARD, no path: VERDICT command-exit 0 (the lock held 43m18s for spec test, typecheck, test:repo and dogfood together). Test Files 404 passed, 3 skipped (407); Tests 3326 passed, 28 skipped (3354). No failure. The package-first-authoring hook timeout seen on 263f6c89 (which ran beside the gate loop) did not recur; this battery ran alone.
Derived gate families and declaration checks (on 62f265f)
  • node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack: 104 commands (74 pnpm, 30 direct node); change set 9 paths vs merge base b3d14150, +541 / -20.
  • All 104 exit 0, each exit captured before any pipe. dispatch-gates --ran: "104 derived famil(ies) accounted for — 104 run, 0 NOT-MEASURED (a DERIVED zero — all 104 recorded an exit code and none of them is 3)".
  • check-adr-0087-registration.mjs --base origin/main (one of the 104): "1 declared-breaking changeset(s), each carrying an ADR-0087 disposition", registered translation-locale-case-canonicalized.
  • check-changeset-no-major.mjs --base origin/main --event with this title and body: exit 0, no (narrowing) / narrowed, direction arm narrowing, no major.
  • check-partof-closing-keyword.mjs with this body as PR_BODY: exit 0.
  • check-governed-merges.mjs --branch HEAD: 0 of 9 paths governed, 561 changed lines.

Carried from #29190 (head 263f6c89, base 6b3ba82c)

Head 263f6c89 (base 6b3ba82c). Round 3 is two commits on top of round 2's 3b3284c9: 713814b9 (the case fold as its own D2 entry, the S2 text, the docs and changeset lines that name it) and 263f6c89 (the census pin's JOIN row, and the two collision messages naming the release). The spec suite, build, consumer packages, ablations, gate families and dogfood battery below were run on 263f6c89. The runtime, rest, downstream-contract, lint, cli and metadata readings and the end-to-end proof were taken on 102f342e and carry: round 3 moves no reader outside packages/spec, and the end-to-end path is pinned at the spec level by the stored-row seam test. Every heavy run went through os-verify-lock.sh; its VERDICT line is quoted.

packages/spec: suite, type check, build, generated artifacts
  • pnpm --filter @objectstack/spec test && pnpm --filter @objectstack/spec typecheck (typecheck includes check:scripts-typecheck and check:test-typecheck): Test Files 789 passed, 1 skipped (790); Tests 23384 passed, 1 skipped, 1 todo. VERDICT command-exit 0. The skipped file is scripts/root-entry-type-nameability.pin.test.ts, which skips itself while spec's dist is stale (it was: the merge fallback had just been built in this worktree). Rerun after the build below: 1 file, 2 tests passed.
  • pnpm --filter @objectstack/spec build && pnpm --filter @objectstack/spec check:generated: "All 14 generated artifacts are up to date". VERDICT command-exit 0. Nothing regenerated: the published JSON Schema of a locale and of both bundles does not move.
  • Targeted, vitest run src/system/translation.test.ts src/conversions src/migrations (on 713814b9): Test Files 29 passed (29), Tests 1184 passed. src/shared/retired-key-removal-version.pin.test.ts and scripts/conversions-major18-merge.test.ts (on 263f6c89): 289 tests passed together with translation.test.ts.
  • pnpm turbo run build --concurrency=2 --filter='./packages/**': Tasks 78 successful, 78 total. VERDICT command-exit 0.
  • eslint --no-inline-config over the changed .ts files: exit 0.
Consumer packages

On 263f6c89:

  • @objectstack/core (the i18n fallbacks; readAuthoredTranslationLayer replays the chain): Test Files 129 passed (129), Tests 2909 passed. VERDICT command-exit 0.
  • @objectstack/metadata-protocol (the save door, the seed loader, stored-row conversions): Test Files 297 passed, 4 skipped (301), Tests 22106 passed, 26 skipped. VERDICT command-exit 0.
  • @objectstack/service-i18n: Test Files 4 passed (4), Tests 71 passed. VERDICT command-exit 0.

Carried from 102f342e: @objectstack/runtime 513/513 files, 8272 passed + 19 skipped; @objectstack/rest 366/366, 6944 passed + 332 skipped; @objectstack/downstream-contract 3/3, 31; @objectstack/lint src/lint-liveness-properties.test.ts 111/111; @objectstack/cli test/metadata-type-schema-gate.test.ts (integration project) 8/8; @objectstack/metadata src/serializers/typescript-serializer-annotation.test.ts 7/7. Each VERDICT command-exit 0.

End-to-end: a zh-cn save reaches a zh-CN session (carried from 102f342)

A one-off scratch test (deleted afterwards): the real saveMetaItem over a stub engine saves { name: 'zh_cn', locale: 'zh-cn', objects: { project: { label: '项目(lower)' } } }; core's readAuthoredTranslationLayer reads the stored rows; createMemoryI18n is loaded with the stock zh-CN strings and then the layer.

E2E save.success= true stored rows= 1 stored.locale= zh-cn layer keys= ["zh-CN"] zh-CN serves= 项目(lower)

On main (6b3ba82c) the same stored row gave layer keys= ["zh-cn"] zh-CN serves= 项目. On 263f6c89 the same seam is pinned in translation.test.ts (applyConversionsToStoredItem reads zh-cn and zh_cn back as zh-CN), and core's suite above is green.

Ablations

Round 3, on 263f6c89 in a side worktree, each through ablation-replace on packages/spec/src/conversions/registry.ts and restored to the HEAD blob (966e69e5). Command: vitest run src/system/translation.test.ts src/conversions src/shared/retired-key-removal-version.pin.test.ts (25 files, 1049 tests green at HEAD).

  • The case entry's row order: 76.5 → order: 75.5 (before spec: a translation item accepts any locale string ("not a locale!!" saves 200, valid) — LocaleSchema is a bare z.string() #25822's entry): 1 failed, 1048 passed (the stored-row seam: zh_cn stays zh-cn, because the case entry runs before the _ is rewritten).
  • The case entry's return parsed.success ? parsed.data : undefined; → return parsed.success ? v : undefined; (shape still checked, case no longer folded): 4 failed, 1045 passed (the stored-row seam, the collision test, the census row's TODO, the entry's fixture).
  • The census row, by hand with the file restored from a copy: dropping the release from the bundle refusal, then from the conversion's TODO, each turned the translation-locale-tag-case-folded row red ("names no release"), 1 failed of 60.

Round 1 (on 102f342e, still applicable to the parse side): the case fold .overwrite(canonicalLocaleCase) → identity gave 14 failed; the refusal's first-key lookup → undefined gave 2 failed (both bundle refusal tests); the key fold folded[canonicalLocaleCase(key)] → folded[(key)] gave 2 failed.

Derived gate families, declaration checks and the dogfood battery (on 263f6c8)
  • node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack at 263f6c89: 105 commands (75 pnpm, 30 direct node), the same list as at 3b3284c9; change set 9 paths vs merge base 6b3ba82cb, 565 changed lines (+543 / -22).
  • All 105 exit 0, each exit captured before any pipe. dispatch-gates --ran: "105 derived famil(ies) accounted for — 105 run, 0 NOT-MEASURED (a DERIVED zero — all 105 recorded an exit code and none of them is 3)".
  • check-adr-0087-registration.mjs --base origin/main (one of the 105): exit 0, "1 declared-breaking changeset(s), each carrying an ADR-0087 disposition", .changeset/28993-locale-case-canonicalized.md [BREAKING+bang+clause-②-narrowing+metadata-protocol-narrowed] registered translation-locale-case-canonicalized (new here: translation-locale-case-canonicalized).
  • check-changeset-no-major.mjs --base origin/main --event (this body and title, offline): exit 0, "LEVEL AXIS: this PR declares clause-② no (narrowing) and metadata-protocol narrowed", direction arm narrowing (BREAKING, ships minor during the launch window), @objectstack/spec: minor, no major.
  • check-partof-closing-keyword.mjs with this body as PR_BODY (offline): exit 0.
  • Dogfood battery, pnpm exec turbo run build --concurrency=2 --filter='@objectstack/dogfood^...' && pnpm --filter @objectstack/dogfood test under the lock (no OS_TEST_SHARD, no path), VERDICT command-exit 1 (held 36m56s): closure Tasks 68 successful, 68 total; Test Files 1 failed, 383 passed, 3 skipped (387); Tests 3161 passed, 33 skipped (3194). The one red file is test/package-first-authoring.dogfood.test.ts: its first beforeAll (bootStack(crmStack)) hit the 10000 ms hook timeout, and its other three describes then refused to boot because com.example.crm "already has a live bootStack kernel in this process" (5 tests skipped, hence 33 skipped against round 2's 28). The battery ran beside the 105-gate loop on the same 4 cores. That file alone under the lock, twice: Test Files 1 passed, Tests 5 passed, both times (VERDICT command-exit 0, held 41 s and 36 s). Reported as found; nothing skipped or narrowed.

Human Minimum

  • 1. Schemas start as Zod; packages/spec holds no runtime logic.
  • 2. Every name I added uses the casing for its kind.
  • 3. No formatter ran; pnpm lint passes.
  • 4. One issue, one branch off an up-to-date main, one pull request.
  • 5. A change that publishes carries a changeset, and both declaration lines are filled in here and in the changeset.
  • 6. packages/spec touched: its generated artifacts are regenerated and committed.
  • 7. No hand edits to content/docs/references/, packages/*/CHANGELOG.md or content/docs/releases/.
  • 8. Title, body and comments are in English.
  • The Before Submitting list in CONTRIBUTING.md is done.

Generated by Claude Code

🤖 Generated with Claude Code

…BCP-47 case

LocaleSchema now returns each tag in its BCP-47 case (zh-cn -> zh-CN,
zh-hans-cn -> zh-Hans-CN, EN -> en) instead of accepting the authored case,
so a translation saved as zh-cn is merged into the zh-CN locale a session
asks for. The fold is case-only: Intl.getCanonicalLocales also replaces
legacy subtags and throws on tags the shape accepts.

The two bundle records judge their keys together and refuse two keys that
fold to one tag, naming both, rather than letting the later one replace the
earlier. The #25822 stored-row conversion folds case too, so a row the save
door stored as zh-cn is read back as zh-CN by every rehydration seam. New
semantic entry translation-locale-case-canonicalized; the stale "case is not
judged" sentences are corrected.

Claude-Session: https://claude.ai/code/session_01ADHkmRmpbFs6uQ8AuadsTr
Co-authored-by: Claude <noreply@anthropic.com>
…efinements ledger

The refusal of two locale keys that fold to one tag is a superRefine with no
JSON Schema form, so the published TranslationBundle / PlatformTranslationBundle
(and the four installed-package envelopes that embed manifest.translations)
gain one dropped-refinement site each. Sites as the build-schemas gate printed
them; measured totals recounted.

Claude-Session: https://claude.ai/code/session_01ADHkmRmpbFs6uQ8AuadsTr
Co-authored-by: Claude <noreply@anthropic.com>
…wing

The refusal of a translation bundle whose two keys fold to one tag refuses
an input that parsed before, so the changeset now carries the amended
declaration lines (Clause-② no (narrowing), Metadata-protocol narrowed),
a BREAKING banner with its FROM -> TO migration, and the ADR-0087
disposition for the new D3 entry.

Claude-Session: https://claude.ai/code/session_01ADHkmRmpbFs6uQ8AuadsTr
Co-authored-by: Claude <noreply@anthropic.com>
…d after 18.2.0

The case half of `translation-locale-tag-canonicalized` rode that entry's
retiredAfter 18.1.0, so an artifact the released 18.2.0 spec built with a
`zh-cn` key fell outside the ingestion window. The case fold is now the D2
entry `translation-locale-tag-case-folded`, stamped retiredAfter 18.2.0 and
ordered right after the underscore entry (`zh_cn` -> `zh-cn` -> `zh-CN`). The
underscore entry is back to its released code; its docblock points at the new
entry and its fixture keeps to its own spellings. The D3 entry and the
changeset now also name the two exact matchers that compare a folded i18n
locale with a row tagged as written (the email-template floor warning and the
messaging template ladder).

Claude-Session: https://claude.ai/code/session_01ADHkmRmpbFs6uQ8AuadsTr
Co-authored-by: Claude <noreply@anthropic.com>
…tions name

`translation-locale-tag-case-folded` is retired after 18.2.0, the newest
published stable release, so the census pin wants a JOIN row with the
prescriptions an author meets for its surface. A lone tag is corrected, not
refused, so those are the collision two keys of one bundle make once folded:
the bundle refusal at the parse and the conversion's TODO for a stored pair.
Both now name the release (`@objectstack/spec 18`). The pin gains a todo row
kind for a data-at-rest prescription with no parse refusal to read.

Claude-Session: https://claude.ai/code/session_01ADHkmRmpbFs6uQ8AuadsTr
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 Nov 3, 2026
@github-actions

github-actions Bot commented Nov 3, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/spec, touching 22 documentable anchor(s). ⚠️ 1 changed file(s) yielded no anchor (packages/spec/dropped-refinements.baseline.json), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

14 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 crm_lead (literal, a string literal in fixture))
  • content/docs/ai/agents.mdx (via crm_lead (literal, a string literal in fixture))
  • content/docs/api/client-sdk.mdx (via crm_lead (literal, a string literal in fixture))
  • content/docs/api/error-catalog.mdx (via crm_lead (literal, a string literal in fixture))
  • content/docs/automation/email-templates.mdx (via defaultLocale (literal, a string literal in apply))
  • content/docs/data-modeling/index.mdx (via crm_lead (literal, a string literal in fixture))
  • content/docs/deployment/environment-variables.mdx (via fallbackLocale (literal, a string literal in apply))
  • content/docs/deployment/validating-metadata.mdx (via crm_lead (literal, a string literal in fixture))
  • content/docs/getting-started/examples.mdx (via i18n.supportedLocales (literal, a string literal in apply))
  • content/docs/kernel/services-checklist.mdx (via defaultLocale (literal, a string literal in apply), fallbackLocale (literal, a string literal in apply))
  • content/docs/permissions/authorization.mdx (via crm_lead (literal, a string literal in fixture))
  • content/docs/protocol/kernel/i18n-standard.mdx (via LocaleSchema (symbol, a top-level const), defaultLocale (literal, a string literal in apply), fallbackLocale (literal, a string literal in apply))
  • content/docs/ui/index.mdx (via crm_lead (literal, a string literal in fixture))
  • content/docs/ui/translations.mdx (via defaultLocale (literal, a string literal in apply), fallbackLocale (literal, a string literal in apply))

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

  • content/docs/releases/v13.mdx (via crm_lead (literal, a string literal in fixture))
  • content/docs/releases/v17/17-5.mdx (via retiredAfter (symbol, a field of const object translationLocaleTagCaseFolded), retiredFromLoadPath (symbol, a field of const object translationLocaleTagCaseFolded), crm_lead (literal, a string literal in fixture))

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
  • 1 changed file(s) yielded no anchor (packages/spec/dropped-refinements.baseline.json) — pages documenting those are invisible to this run
  • 6 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 61 of 219 client-bound route-ledger rows — the other 158 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 158: 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; 103 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 — 168 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 a7e6e07ff9bb777b688c0424da1cdd9790e11f5f → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 730db33ae6afd62ddb4ebfb250310f9d4b983f13 — the merge of head 62f265f03245d8118b52b7dfdb461208ea52dc06 into base a7e6e07ff9bb777b688c0424da1cdd9790e11f5f, 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 730db33ae6afd62ddb4ebfb250310f9d4b983f13 && git checkout 730db33ae6afd62ddb4ebfb250310f9d4b983f13
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin a7e6e07ff9bb777b688c0424da1cdd9790e11f5f 62f265f03245d8118b52b7dfdb461208ea52dc06 && git checkout -B drift-repro a7e6e07ff9bb777b688c0424da1cdd9790e11f5f && git merge --no-ff 62f265f03245d8118b52b7dfdb461208ea52dc06

node scripts/docs-audit/affected-docs.mjs --json a7e6e07ff9bb777b688c0424da1cdd9790e11f5f

⚠️ 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 a7e6e07ff9bb777b688c0424da1cdd9790e11f5f → 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

domain:spec#2 · os-support-ai · session_01WGufzhJG3tKnksHVa1krBz

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 62f265f03245d8118b52b7dfdb461208ea52dc06
Local-runs: none
Scope: packages/spec/src/conversions/registry.ts, packages/spec/src/migrations/entries/semantic/18.translation-locale-case-canonicalized.ts, packages/spec/src/migrations/entries/semantic/18.translation-locale-non-tag-refused.ts, packages/spec/src/system/translation.zod.ts, packages/spec/src/shared/retired-key-removal-version.pin.test.ts, packages/spec/src/system/translation.test.ts, packages/spec/dropped-refinements.baseline.json + packages/spec/scripts/lib/dropped-refinements.ts, packages/spec/scripts/build-schemas.ts, packages/spec/scripts/dropped-refinements.test.ts, packages/spec/src/conversions/types.ts, packages/spec/src/conversions/apply.ts, packages/spec/src/conversions/stored.ts, packages/spec/src/conversions/retired-after.census.test.ts, packages/spec/src/conversions/conversions.test.ts, packages/spec/src/migrations/registry.ts.template, packages/spec/src/stack.zod.ts, packages/services/service-messaging/src/template-renderer.ts, packages/spec/package.json

Delta review over the two at-tier PASS records on PR #29190 at 263f6c891eac65f49418adc1f13aa7004bf6b9be: 6577781599 (domain:spec#1) and 6577787009 (triage#1). PR #29833 supersedes the closed draft #29190 (closed 2026-11-03T06:43:39Z, unmerged, head 263f6c89). Every judgment of those two records not restated below stands and is adopted for 62f265f0. Inputs: git diff origin/main...62f265f0 against git diff 6b3ba82c 263f6c89 file by file; the merge commit's two parents; git merge-tree --write-tree b3d14150 263f6c89; every comment on #28993 through 6597236864 and the ruling 6595806433 (letter A); the PR body of #29833; the check-runs on 62f265f0, polled until none was pending. Nothing was built, run or checked out; the template was printed with record-recognisers.mjs --template.

① Derived judgments

  1. The head is feat(spec)!: a locale tag written in another case is corrected to its BCP-47 case #29190's five commits plus one merge, and nothing else — RIGHT. 62f265f0 has parents 263f6c89 (feat(spec)!: a locale tag written in another case is corrected to its BCP-47 case #29190's head) and b3d14150 (main at merge time; origin/main is now one commit further, a7e6e07f, which touches none of the nine files). The five non-merge commits 5397e120, 102f342e, 3b3284c9, 713814b9, 263f6c89 have stable patch-ids identical to feat(spec)!: a locale tag written in another case is corrected to its BCP-47 case #29190's five. git merge-tree --write-tree b3d14150 263f6c89 conflicts in exactly one path, packages/spec/dropped-refinements.baseline.json, and the head's tree differs from the merge-tree result in that one file only: registry.ts and the pin test auto-merged, and the head carries the auto-merge result byte for byte.
  2. The net diff is the same change the earlier records judged, file by file — RIGHT. Nine files on both sides, same list. Six files (.changeset/28993-locale-case-canonicalized.md, i18n-standard.mdx, both semantic entries, translation.test.ts, translation.zod.ts) have byte-identical hunks, because main did not move their base. registry.ts (191 changed lines) and retired-key-removal-version.pin.test.ts (33 changed lines) have identical added and removed lines; only hunk headers and one context line move (main added +321/−13 to the registry and +13/−1 to the pin test under them). No hunk differs in content. The one file whose hunks differ is the ledger, judged in ①3.
  3. The ledger resolution — RIGHT, recounted against the tree. The gate (scripts/lib/dropped-refinements.ts#checkDroppedRefinements, called by build-schemas.ts) compares each entry's sites as a set plus its length against the build's census; order inside a list is not judged; scripts/dropped-refinements.test.ts pins measured.publishedSchemasWithDroppedRefinements to the number of entries and measured.droppedRefinementSites to the sum of sites.length. Parsed both files: main has 250 entries / 1012 sites, the head 250 / 1018, header fields equal to the body on both; the entry key order is identical to main's; description, zod, refinementSitesThatDidProject (369) and refinementSitesWithNoJsonFormToCompare (0) unchanged. Entry-by-entry set difference main → head: api/AssembledInstalledPackage +manifest.translations.element (78 → 79), api/GetInstalledPackageResponse +data.options[1].manifest.translations.element (74 → 75), api/InstalledPackageAtEitherStage +options[1].manifest.translations.element (78 → 79), api/ListInstalledPackagesResponse +data.packages.element.options[1].manifest.translations.element (72 → 73), system/PlatformTranslationBundle and system/TranslationBundle +"" (6 → 7 each); no removal in any entry, no entry lost, no duplicate path introduced, no other entry touched. Those are exactly the six sites feat(spec)!: a locale tag written in another case is corrected to its BCP-47 case #29190 added over 6b3ba82c (same six entries, same six paths, 983 → 989 there). The four api/* lists grew by four each on main between the two bases (fix(spec)!: a dashboard that declares two global filters under one name is refused #29162 and later), which is the whole of the conflict; main's re-sort of notices.valueType.message is kept as main has it. The ledger is hand-edited by design (no generator; build-schemas.ts says so), so a hand resolution is the prescribed shape, and the dev's check:generated --fix touching nothing is consistent with that.
  4. Nothing main brought in since 263f6c89 contradicts the earlier judgments — RIGHT. translation.zod.ts, translation.test.ts, both semantic entries, conversions/types.ts, conversions.test.ts, retired-after.census.test.ts, walk.ts, artifact-forward-conversion.ts, authored-translation-sync.ts and memory-i18n.ts are untouched on main since 6b3ba82c. The registry on main gained two D2 entries at retiredAfter: '18.2.0' (form-view-drawer-keys-removed, order 81; view-personal-declarations-removed, order 82) and nothing near 76 / 76.5; the head's MAJOR_18_CONVERSIONS holds translationLocaleTagCanonicalized at 76 and translationLocaleTagCaseFolded at 76.5 with no duplicate order, inApplicationOrder unchanged, CONVERSIONS_BY_MAJOR[18] built from it. @objectstack/spec is still 18.2.0 on main and on the head, so the new entry's stamp is still at the newest published stable and the pin test's first guard still demands its JOIN row. The merged pin test holds main's two new rows (form-view-drawer-keys-removed, view-personal-declarations-removed, with the ViewScopeSchema / ViewSharingSchema import main added) beside this PR's translation-locale-tag-case-folded row and its todo renderer; the names no release guard is present once. apply.ts and stored.ts changed only docblocks (a retirement kit ages out one major after it retires, ruling 6434038284) and stored.ts added object_extension to STORED_ONLY_COLLECTIONS; neither moves a toMajor-18 entry replayed on an 18 runtime, and translations stays in that map. refused-shapes.ts gained nothing naming a translation or locale. migrations/registry.ts.template gained two text entries (orders 116, 117) for main's retirements; this PR adds no template entry, exactly as feat(spec)!: a locale tag written in another case is corrected to its BCP-47 case #29190 did not. stack.zod.ts#warnEmailTemplateLocaleFloor and template-renderer.ts#localeCandidates, the two exact matchers the D3 entry names, are still there under the names the entry gives.
  5. The ruling is what the head carries — RIGHT. 6595806433 (letter A, maintainer 同意 2026-11-03T04:55Z) binds: a TranslationBundle / PlatformTranslationBundle holding two keys that fold to one tag is refused at the parse naming both keys and the tag; declared Clause-②: no (narrowing) · Metadata-protocol: narrowed; B not taken. At the head, both bundle schemas are z.record(LocaleTagKeySchema, …).superRefine(refuseLocaleKeysFoldingTogether).overwrite(foldLocaleKeys), and the refinement adds a custom issue at the second key whose message names first, key and tag and the release (Refused since @objectstack/spec 18); the prepared merge swap is absent from the diff. The card body carries Ruled: 6595806433 · letter A and Ruling-ref: 6595806433; the PR body carries Ruling-ref: 6595806433 and Supersedes #29190.

② Semver level

Unchanged by the merge and CORRECT. The changeset at the head: '@objectstack/spec': minor, title feat(spec)!:, Clause-②: no (narrowing), Metadata-protocol: narrowed, one BREAKING banner with its FROM → TO migration (a bundle holding both spellings → one canonical key, merged by hand), exactly one ADR-0087 marker registered translation-locale-case-canonicalized. The PR body carries the same two declaration lines and the same title. The diff adds no authorable key, removes none, moves no published JSON Schema (the ledger's +6 sites are the superRefine with no JSON form, at the bundle roots and the four api/*InstalledPackage* embeddings); the one refused input is the colliding pair, so narrowed over semantic and (narrowing) ⇒ BREAKING at minor. Check Changeset is success on this head.

③ Boundary flags

  • Gate verdicts on 62f265f0: 69 check-runs, all completed at 07:03Z (polled from 06:48Z, 16 polls): 58 success, 10 skipped (Lint UI, Auto Label, Check PR Size, Dogfood Verify CLI, the Dogfood Regression Gate shard matrix, Temporal Conformance, UI Published Artifacts, the schedule gate, the two opt-in harnesses — the same skip set as on 263f6c89), 1 failure, 0 pending. Check Changeset, Build Core, every Test Core shard, every Type Check and Lint job are success.
  • The PR-body guards failure at 06:41:09Z–06:41:39Z is the duplicate Fixes #28993 while feat(spec)!: a locale tag written in another case is corrected to its BCP-47 case #29190 was still open (feat(spec)!: a locale tag written in another case is corrected to its BCP-47 case #29190 closed 06:43:39Z); a second PR-body guards run started 06:44:19Z is success. Read as superseded, as the dispatch said to expect.
  • PR state: draft, mergeable: null / mergeable_state: unknown at read time (GitHub had not yet computed it against a7e6e07f); git merge-tree of the head onto origin/main is outside this record's scope, but a7e6e07f touches none of the nine files, so no new conflict follows from it.
  • Ledger counts in the PR body (1012 → 1018 on b3d14150; 983 → 989 on 6b3ba82c) match the recount in ①3.
  • Dogfood battery on the resumed head (dev report 6597236864): command-exit 0, 404 files passed, 3 skipped, no failure; the package-first-authoring boot timeout noted on 263f6c89 did not recur. A dev-reported local run, not a gate verdict; noted, nothing to escalate.
  • Carried from 6577781599 / 6577787009 unchanged: the save door stores the authored body (domain:server sibling card); memory-i18n.ts#resolveLocale untouched; nothing under packages/metadata-protocol touched; the generated registry.ts not hand-edited beyond the entries the records judged; the mdx a bounded in-place fix outside the claim surface; the ninth file (the pin test) forced by the census guard. The open question those records left to the maintainer is now ruled (①5), so it is closed, not carried.
  • Seat identity: the earlier records' Implemented-by named claude/project-thread-ih2cyh; the resumed branch is claude/project-thread-bo9b0g, holding the same five commits (patch-id identical) plus the merge. This record names the branch that produced the reviewed head.

④ Design judgment

On a merge-only delta the design judgments of 6577781599 (④, every hits file) and 6577787009 (④) are adopted whole; nothing in the merge moves the fold, the key twin, the refusal, the D2 entry, its order, its stamp, the D3 entry or the pin row, and ①4 reads the main-side changes that could have moved their premises and finds none did. Re-read on this head and holding: MAJOR_18_CONVERSIONS still sorts by numeric order with ties by id, so 76 then 76.5 still gives zh_cn → zh-cn → zh-CN; the census pin still derives the release from retiredAfter at the newest published stable (18.2.0 on both sides), so the todo row is still demanded and still satisfied by a message naming @objectstack/spec 18; the three entries stamped 18.2.0 on the merged head each carry a JOIN row.

packages/spec/dropped-refinements.baseline.json (the one hand-resolved file) — judged against the gate that reads it (checkDroppedRefinements, set-and-length per entry; the ledger test's header-equals-body pin). HOLDS: ①3 is the full recount. No finding.

No must-fix. No should-fix.

Implemented-by: claude/project-thread-bo9b0g
Reviewed-by: session_01WGufzhJG3tKnksHVa1krBz

VERDICT: PASS

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:spec#2 · os-support-ai · session_01WGufzhJG3tKnksHVa1krBz

席位核对 — ACCEPT · domain:spec#2 · head 62f265f03245d8118b52b7dfdb461208ea52dc06

  • Shape: PR feat(spec)!: a locale tag written in another case is corrected to its BCP-47 case #29190's five commits unchanged (same patch-ids) plus one merge of origin/main: a locale tag written in another case is corrected to its BCP-47 case at the parse, and a bundle holding two keys that fold to one tag is refused naming both keys and the tag; the D3 entry, the ADR-0087 dispositions, the i18n standard text, the dropped-refinements ledger. 9 files, +541 −20.
  • Ruling: 6595806433, letter A (maintainer 「同意」 2026-11-03T04:55Z): refuse, never merge silently; the saveMetaItem canonical write stays the domain:server sibling card.
  • Contract review: records 6577781599 and 6577787009 (CONTRACT_REVIEW_TIER, head 263f6c89, PASS) and delta record 6597563334 (head 62f265f0, PASS, no findings). The merge's one conflict, dropped-refinements.baseline.json, recounted: main 250 entries / 1012 sites → 250 / 1018, exactly this PR's six sites, none lost.
  • Declarations: Clause-②: no (narrowing), Metadata-protocol: narrowed, @objectstack/spec minor, BREAKING with FROM → TO and one ADR-0087 marker.
  • Dogfood pinned to the final head: the executor's locked run read 62f265f0: 404 files passed, 3 skipped, 0 failed; 3326 tests passed, 28 skipped.
  • Gates: every check-run on the head complete; the one red, PR-body guards at 06:41Z (duplicate Fixes while feat(spec)!: a locale tag written in another case is corrected to its BCP-47 case #29190 was open), is superseded by its green re-run at 06:44Z.
  • Accepted deviations: a new branch and PR superseding feat(spec)!: a locale tag written in another case is corrected to its BCP-47 case #29190 (the original card thread is gone); feat(spec)!: a locale tag written in another case is corrected to its BCP-47 case #29190 closed with a pointer (6597248855).
  • Landing: not governed, 561 changed lines. Ready + auto-merge now.

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