Repository navigation
feat(spec)!: a locale tag written in another case is corrected to its BCP-47 case - #29190
objectstack-fleet[bot] wants to merge 5 commits into
Conversation
…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>
📓 Docs Drift CheckThis PR changes 1 package(s): 13 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 2 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 168 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 7014166509325a1182c15d627683e88b6ed6e8ae && git checkout 7014166509325a1182c15d627683e88b6ed6e8ae
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin a19759ac83745bde6f2e985e5f8cde0b4da96611 263f6c891eac65f49418adc1f13aa7004bf6b9be && git checkout -B drift-repro a19759ac83745bde6f2e985e5f8cde0b4da96611 && git merge --no-ff 263f6c891eac65f49418adc1f13aa7004bf6b9be
node scripts/docs-audit/affected-docs.mjs --json a19759ac83745bde6f2e985e5f8cde0b4da96611
|
…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>
|
domain:spec#1 · os-steve · session_01ADHkmPi25XXKx92Xmg4viS Head-sha: 维护者速读
席位意见:可落地,等你选 + 契约复审。 席位核对 — ACCEPT ·
|
|
triage#1 · os-zhuang · session_01AavokzJ5DndAwitDXvKy4U Contract-review-claim: Taken on the lane's Generated by Claude Code |
|
triage#1 · os-zhuang · session_01AavokzJ5DndAwitDXvKy4U Contract reviewServed-tier: Inputs read: card #28993 body and all six comments (triage 6570179836, claim 6571022743, dev reports 6571338866 / 6573167205 / 6574216803, claim amendment 6573207423); PR #29190 body, file list, both PR comments and the net diff against ① Derived judgments
② Semver level
One wording nit, not a gate: the changeset says the D3 entry "carries both halves to ③ Boundary flags
④ Design judgment1.
2.
3. 4. Premature part, and whether it must wait. The case fold ( Implemented-by: VERDICT: PASS Generated by Claude Code |
|
domain:spec#1 · os-steve · session_01ADHkmPi25XXKx92Xmg4viS Contract-review-claim: session_01ADHkmPi25XXKx92Xmg4viS · head 3b3284c · 2026-11-02T10:44Z Triage ruling 6575141496 returned contract review to the lane, so the spec seat reviews this head itself at |
|
domain:spec#1 · os-steve · session_01ADHkmPi25XXKx92Xmg4viS Contract reviewServed-tier: Judged on the head's tree, the card's body and every comment on #28993, and the check-runs on the head (every non-skipped run ① Derived judgments
② Semver level
Judgment: CORRECT. The diff refuses a value that parsed before (the colliding pair) and changes what a parse returns (the case fold): under the checklist's precedence ③ Boundary flags
④ Design judgment
Floor paths outside
No must-fix. Two should-fixes, neither on the card's own path. Implemented-by: VERDICT: PASS |
…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>
|
domain:spec#1 · os-steve · session_01ADHkmPi25XXKx92Xmg4viS Contract-review-claim: session_01ADHkmPi25XXKx92Xmg4viS · head 263f6c8 · 2026-11-02T12:59Z |
|
triage#1 · os-zhuang · session_01AavokzJ5DndAwitDXvKy4U Contract-review-claim: session_01AavokzJ5DndAwitDXvKy4U · head 263f6c8 · 2026-11-02T12:59Z Taken under (b): the head has waited 90 min since the spec seat's PASS at Generated by Claude Code |
|
domain:spec#1 · os-steve · session_01ADHkmPi25XXKx92Xmg4viS Contract reviewServed-tier: Delta review. The PASS record on Delta against ① Derived judgmentsCarried from the record on
② Semver levelUnchanged from ③ Boundary flags
④ Design judgment
Floor paths outside No must-fix. No should-fix. Both round-1 should-fixes are taken as asked. Implemented-by: VERDICT: PASS |
|
triage#1 · os-zhuang · session_01AavokzJ5DndAwitDXvKy4U Contract reviewServed-tier: Inputs read:
A delta review. Every judgment of 6575488221 and 6574667011 not restated below stands, and this record adopts them for ① Derived judgments
② Semver levelUnchanged by the delta. The changeset keeps ③ Boundary flags
④ Design judgmentSplitting the case fold into its own entry is the ADR-0087-true shape. A retired entry's Tried to break it:
Must-fix: none on the reviewed head. Landing condition (not a design finding): the head is Should-fix: none new. VERDICT: PASS Generated by Claude Code |
|
domain:spec#2 · os-support-ai · session_01WGufzhJG3tKnksHVa1krBz Superseded by #29833: the same five commits through |
domain:spec#1 · os-steve · session_01ADHkmRmpbFs6uQ8AuadsTr
Requested by Steve · project thread
Fixes #28993
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 conversiontranslation-locale-tag-case-folded, and a bundle with two keys that fold to one tag is refused).skills/objectstack-i18n/SKILL.mdalready spells every tag in its canonical case and teaches no case rule, so no skill text or eval moves.维护者速读
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包总是先命中。现在解析层只留一种写法,每个读者看到同一个标签。zh-cn和zh-CN)以前能通过,现在会被拒绝并点名两个键,仓内命中 0 处。大小写更正本身是放宽,其它原来能通过的输入照样通过,JSON Schema 不变。还有两处按原写法精确比较的地方(邮件模板的语言下限告警、通知模板的语言梯):配置里写成zh-cn的默认或支持语言现在读作zh-CN,存量模板行若仍标zh-cn就对不上,需要把模板行改成标准写法,行侧归位随 server 车道的入口卡落地。如果你选合并,一笔提交就能换成「按标准写法的键优先合并」,声明行回到 yes (widening) / semantic,不再是破坏性改动。保存入口仍原样存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 anylocalestring ("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
LocaleSchemacorrects case (packages/spec/src/system/translation.zod.ts). A tag that passesLOCALE_TAG_SHAPEis 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), soz.inputandz.outputare bothstring,Localestaysz.input, and the published JSON Schema does not move (check:generatedregenerated nothing). A canonical tag is returned unchanged, and nothing that parsed before is refused except the bundle case below.Intl.getCanonicalLocales. Measured on Node v22.23.4:Intl.getCanonicalLocalesrewrites 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 tagsLOCALE_TAG_SHAPEaccepts (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.locale;TranslationBundle/PlatformTranslationBundlekeys;i18n.defaultLocale,fallbackLocale,supportedLocales; a seed dataset'slocalescope (data/seed.zod.ts) and the seed loader'slocale(data/seed-loader.zod.ts).EmailTemplateDefinitionSchema.localedoes not useLocaleSchemaand 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.z.record(LocaleTagKeySchema, …).superRefine(refuseLocaleKeysFoldingTogether).overwrite(foldLocaleKeys). The key schema is a shape-only twin ofLocaleSchema(same regex, message and description, no fold): a folding key schema would letz.recordkeep 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 acustomissue 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.packages/spec/src/conversions/registry.ts). The new ADR-0087 D2 conversiontranslation-locale-tag-case-folded(toMajor: 18,retiredFromLoadPath: true,retiredAfter: '18.2.0', the same surface as spec: a translation item accepts anylocalestring ("not a locale!!" saves 200, valid) — LocaleSchema is a bare z.string() #25822's entry) folds a tag in another case to theLocaleSchemaparse: an item'slocale,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 byrenameKey. Itsorderis 76.5, right after spec: a translation item accepts anylocalestring ("not a locale!!" saves 200, valid) — LocaleSchema is a bare z.string() #25822'stranslation-locale-tag-canonicalized(76), so an underscore spelling is made a tag first and folded second (zh_cn→zh-cn→zh-CN).localestring ("not a locale!!" saves 200, valid) — LocaleSchema is a bare z.string() #25822's entry keeps its own stamp.translation-locale-tag-canonicalizedstaysretiredAfter: '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).saveMetaItem) stores the authored body, so azh-cnsave stayszh-cnat rest; core'sreadAuthoredTranslationLayerreplays the full conversion chain over each stored row before keying it bydata.locale, so the row reacheszh-CNsessions from the next sync on, with nothing to re-save. An artifact built by@objectstack/spec18.2.0 or earlier is folded the same way inside its declared-floor window.warnEmailTemplateLocaleFloorinstack.zod.ts) comparesi18n.supportedLocaleswithemailTemplates[].locale, and the messaging template ladder compareslocaleCandidates(i18n.getDefaultLocale())withsys_notification_template.locale. A row taggedzh-cnno longer matches a supported or default locale authored aszh-cn, which now readszh-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.packages/spec/src/migrations/entries/semantic/18.translation-locale-case-canonicalized.ts,conversionIds: ['translation-locale-tag-case-folded']. The sibling entry18.translation-locale-non-tag-refused.tssaid case is not refused because the resolvers fold it; it now says the parse corrects it.content/docs/protocol/kernel/i18n-standard.mdx, one paragraph (seeSkills-docs:). This file is outside the claim's listed surface: a bounded in-place fix of a sentence this diff makes false.packages/spec/dropped-refinements.baseline.json): +6 sites, 983 → 989. The bundlesuperRefinehas no JSON Schema form, so it lands at the root ofsystem/TranslationBundleandsystem/PlatformTranslationBundle, and atmanifest.translations.elementin the fourapi/*InstalledPackage*responses that embed a bundle. Entries are the ones the spec build's gate printed..changeset/28993-locale-case-canonicalized.md,@objectstack/spec: minor, titledfeat(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.memory-i18n.ts#resolveLocale(triage 6570179836: no third runtime case-fold); the save door inpackages/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 anylocalestring ("not a locale!!" saves 200, valid) — LocaleSchema is a bare z.string() #25822'stranslation-locale-tag-canonicalized: its stored itemlocale: 'pt-br'is nowpt-BRon both sides (beforeandafter),expectedNoticesunchanged at 7. The fixture runner replays the whole chain, so apt-brthere 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-foldedis retired after 18.2.0, the newest published stable release, so the census pin requires aJOINrow 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 newtodorow 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 existingaccepts 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 describeLocaleSchema corrects a tag to its BCP-47 case (#28993):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-1996unchanged;en-US-u-CA-Gregory→en-US-u-ca-gregory;en-x-US→en-x-us.iwstaysiw,TL→tl,cmn-cn→cmn-CN, andzh-hans-hans→zh-Hans-Hans(a tagIntl.getCanonicalLocalesthrows on).defineTranslation,TranslationConfig(defaultLocale,fallbackLocale,supportedLocales),SeedSchema.locale,SeedLoaderConfig.locale.TranslationBundleSchema,defineTranslationBundle,PlatformTranslationBundleSchema.zh-cnbesidezh-CNwith exactly[['custom', 'zh-CN']](code, path), and the message names both keys.zh-CN,zh-TW,zh-hans-cn);zh-cnin one bundle andzh-CNin another, each parsed on its own (two bundles ofstack.translationsare separate records the runtime merges per locale).applyConversionsToStoredItem('translation', { locale: 'zh-cn', … })reads backzh-CN; a canonical row reads back equal; an underscoredzh_cnrow reads backzh-CN(the order: spec: a translation item accepts anylocalestring ("not a locale!!" saves 200, valid) — LocaleSchema is a bare z.string() #25822's entry makes it a tag, the case entry folds it).retiredAfteris18.2.0; a bundle withzh-cnbeside a differentzh-CNis left as stored with one TODO attranslations[0].zh-cn; an identicalja-jp/ja-JPtwin is merged with one notice.pt-br→pt-BR, bundle keysko-kr→ko-KRandIT→it,i18nen-us→en-USandzh-hans-cn→zh-Hans-CN,data[].localezh-cn→zh-CN; 7 notices.Acceptance notes
maintoday (both keys kept). After this PR it is refused, naming both keys. Measured on6b3ba82cover 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 isnarrowed,narrowedoversemantic) the seat amended the claim (6573207423) toClause-②: no (narrowing)/Metadata-protocol: narrowed, and this body and the changeset carry those lines. Landing waits for the maintainer's word on the seat's decision card: refuse naming both keys, or merge with the canonical key winning. The merge is prepared as a one-commit swap on top of this head (8 files, +85 / -92: the refinement and its ledger lines go,foldLocaleKeysdeep-merges, the refusal tests become merge tests in both key orders with an unmerged CONTROL, the census row keeps only the TODO, and the prose moves with it); with it the change is non-breaking and the lines go back toyes (widening)/semantic. On that build (one local commit,4f8cfe7a, on263f6c89; the patch is/mnt/project-files/spec-seat1/28993-merge-fallback.patchin the project's files) the targeted spec run (translation, conversions, migrations and the census pin) read 30 files / 1244 tests passed, the spec build andcheck:generatedwere green,check:adr-0087-registrationread "this PR adds no declared-breaking changeset", andcheck-changeset-no-major --eventwith the lines swapped exited 0 (yes (widening)/semantic).saveMetaItem(packages/metadata-protocol/src/protocol.ts) runsschema.safeParse(item)and stores the request body verbatim for every type exceptview, 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.tsand its test describe a bundle asz.record(LocaleSchema, TranslationDataSchema)in comments. The key schema is now a shape-only twin ofLocaleSchema; the comments' point (keys are locale codes, groups live one level down) still holds. Not changed here.Verification
Head
263f6c89(base6b3ba82c). Round 3 is two commits on top of round 2's3b3284c9:713814b9(the case fold as its own D2 entry, the S2 text, the docs and changeset lines that name it) and263f6c89(the census pin'sJOINrow, and the two collision messages naming the release). The spec suite, build, consumer packages, ablations, gate families and dogfood battery below were run on263f6c89. The runtime, rest, downstream-contract, lint, cli and metadata readings and the end-to-end proof were taken on102f342eand carry: round 3 moves no reader outsidepackages/spec, and the end-to-end path is pinned at the spec level by the stored-row seam test. Every heavy run went throughos-verify-lock.sh; itsVERDICTline is quoted.packages/spec: suite, type check, build, generated artifacts
pnpm --filter @objectstack/spec test && pnpm --filter @objectstack/spec typecheck(typecheck includescheck:scripts-typecheckandcheck:test-typecheck): Test Files 789 passed, 1 skipped (790); Tests 23384 passed, 1 skipped, 1 todo. VERDICT command-exit 0. The skipped file isscripts/root-entry-type-nameability.pin.test.ts, which skips itself while spec'sdistis 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.vitest run src/system/translation.test.ts src/conversions src/migrations(on713814b9): Test Files 29 passed (29), Tests 1184 passed.src/shared/retired-key-removal-version.pin.test.tsandscripts/conversions-major18-merge.test.ts(on263f6c89): 289 tests passed together withtranslation.test.ts.pnpm turbo run build --concurrency=2 --filter='./packages/**': Tasks 78 successful, 78 total. VERDICT command-exit 0.eslint --no-inline-configover the changed.tsfiles: exit 0.Consumer packages
On
263f6c89:@objectstack/core(the i18n fallbacks;readAuthoredTranslationLayerreplays 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/runtime513/513 files, 8272 passed + 19 skipped;@objectstack/rest366/366, 6944 passed + 332 skipped;@objectstack/downstream-contract3/3, 31;@objectstack/lintsrc/lint-liveness-properties.test.ts111/111;@objectstack/clitest/metadata-type-schema-gate.test.ts(integration project) 8/8;@objectstack/metadatasrc/serializers/typescript-serializer-annotation.test.ts7/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
saveMetaItemover a stub engine saves{ name: 'zh_cn', locale: 'zh-cn', objects: { project: { label: '项目(lower)' } } }; core'sreadAuthoredTranslationLayerreads the stored rows;createMemoryI18nis loaded with the stockzh-CNstrings 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 gavelayer keys= ["zh-cn"] zh-CN serves= 项目. On263f6c89the same seam is pinned intranslation.test.ts(applyConversionsToStoredItemreadszh-cnandzh_cnback aszh-CN), and core's suite above is green.Ablations
Round 3, on
263f6c89in a side worktree, each throughablation-replaceonpackages/spec/src/conversions/registry.tsand 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).order: 76.5→order: 75.5(before spec: a translation item accepts anylocalestring ("not a locale!!" saves 200, valid) — LocaleSchema is a bare z.string() #25822's entry): 1 failed, 1048 passed (the stored-row seam:zh_cnstayszh-cn, because the case entry runs before the_is rewritten).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).translation-locale-tag-case-foldedrow 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 →undefinedgave 2 failed (both bundle refusal tests); the key foldfolded[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/objectstackat263f6c89: 105 commands (75 pnpm, 30 direct node), the same list as at3b3284c9; change set 9 paths vs merge base6b3ba82cb, 565 changed lines (+543 / -22).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-protocolnarrowed", direction armnarrowing(BREAKING, shipsminorduring the launch window),@objectstack/spec: minor, nomajor.check-partof-closing-keyword.mjswith this body asPR_BODY(offline): exit 0.pnpm exec turbo run build --concurrency=2 --filter='@objectstack/dogfood^...' && pnpm --filter @objectstack/dogfood testunder the lock (noOS_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 istest/package-first-authoring.dogfood.test.ts: its firstbeforeAll(bootStack(crmStack)) hit the 10000 ms hook timeout, and its other three describes then refused to boot becausecom.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
packages/specholds no runtime logic.pnpm lintpasses.main, one pull request.packages/spectouched: its generated artifacts are regenerated and committed.content/docs/references/,packages/*/CHANGELOG.mdorcontent/docs/releases/.Generated by Claude Code