Repository navigation
Commit 86d0a8c
feat(spec)!: a locale tag written in another case is corrected to its BCP-47 case (#29833)
domain:spec#2 · os-support-ai · session_01WGufzhJG3tKnksHVa1krBz
<!-- claude-projects-attribution: {"github_login":"os-support-ai"} -->
_Requested by **Yibo Wang** · [project
thread](https://claude.ai/code/project/chan_01BVUyyPie8gHwoPM18iABwB?thread=cmsg_01BVUyyPie8gHwoPM18iABwBY61hHdLdFdzwXbxaNtv58y)_
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`
会话永远看不到这些翻译(#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`(存量行大小写归位,排在 #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 (#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 (#28896 / #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 #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 #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`).
- **#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 #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:
#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.
<details><summary>Build, spec suites, generated artifacts</summary>
- `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.
</details>
<details><summary>Dogfood battery (on 62f265f)</summary>
- `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.
</details>
<details><summary>Derived gate families and declaration checks (on
62f265f)</summary>
- `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.
</details>
### 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.
<details><summary>packages/spec: suite, type check, build, generated
artifacts</summary>
- `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.
</details>
<details><summary>Consumer packages</summary>
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.
</details>
<details><summary>End-to-end: a zh-cn save reaches a zh-CN session
(carried from 102f342)</summary>
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.
</details>
<details><summary>Ablations</summary>
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 #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.
</details>
<details><summary>Derived gate families, declaration checks and the
dogfood battery (on 263f6c8)</summary>
- `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.
</details>
## Human Minimum
- [x] 1. Schemas start as Zod; `packages/spec` holds no runtime logic.
- [x] 2. Every name I added uses the casing for its kind.
- [x] 3. No formatter ran; `pnpm lint` passes.
- [x] 4. One issue, one branch off an up-to-date `main`, one pull
request.
- [x] 5. A change that publishes carries a changeset, and both
declaration lines are filled in here and in the changeset.
- [x] 6. `packages/spec` touched: its generated artifacts are
regenerated and committed.
- [x] 7. No hand edits to `content/docs/references/`,
`packages/*/CHANGELOG.md` or `content/docs/releases/`.
- [x] 8. Title, body and comments are in English.
- [x] The *Before Submitting* list in CONTRIBUTING.md is done.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01WGufzfTZqxsVg4dG5puUE2)_
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent 06513bc commit 86d0a8c
9 files changed
Lines changed: 541 additions & 20 deletions
File tree
- .changeset
- content/docs/protocol/kernel
- packages/spec
- src
- conversions
- migrations/entries/semantic
- system
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
245 | 245 | | |
246 | 246 | | |
247 | 247 | | |
248 | | - | |
| 248 | + | |
| 249 | + | |
| 250 | + | |
| 251 | + | |
| 252 | + | |
| 253 | + | |
249 | 254 | | |
250 | 255 | | |
251 | 256 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
3 | 3 | | |
4 | 4 | | |
5 | 5 | | |
6 | | - | |
| 6 | + | |
7 | 7 | | |
8 | 8 | | |
9 | 9 | | |
| |||
129 | 129 | | |
130 | 130 | | |
131 | 131 | | |
| 132 | + | |
132 | 133 | | |
133 | 134 | | |
134 | 135 | | |
| |||
280 | 281 | | |
281 | 282 | | |
282 | 283 | | |
| 284 | + | |
283 | 285 | | |
284 | 286 | | |
285 | 287 | | |
| |||
422 | 424 | | |
423 | 425 | | |
424 | 426 | | |
| 427 | + | |
425 | 428 | | |
426 | 429 | | |
427 | 430 | | |
| |||
504 | 507 | | |
505 | 508 | | |
506 | 509 | | |
| 510 | + | |
507 | 511 | | |
508 | 512 | | |
509 | 513 | | |
| |||
1463 | 1467 | | |
1464 | 1468 | | |
1465 | 1469 | | |
| 1470 | + | |
1466 | 1471 | | |
| 1472 | + | |
1467 | 1473 | | |
1468 | 1474 | | |
1469 | 1475 | | |
1470 | | - | |
1471 | | - | |
| 1476 | + | |
1472 | 1477 | | |
1473 | 1478 | | |
1474 | 1479 | | |
| |||
1533 | 1538 | | |
1534 | 1539 | | |
1535 | 1540 | | |
| 1541 | + | |
1536 | 1542 | | |
| 1543 | + | |
1537 | 1544 | | |
1538 | 1545 | | |
1539 | 1546 | | |
1540 | | - | |
1541 | | - | |
| 1547 | + | |
1542 | 1548 | | |
1543 | 1549 | | |
1544 | 1550 | | |
| |||
0 commit comments