fix(spec): reject the retired key/defaultValue inline-map spellings by name, and state the measured resolver behaviour in the rejection message - #10644
Conversation
…me; state the measured resolver behaviour in the message (#10492) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01B4h3medzvhB9rpfoja9jcw
📓 Docs Drift CheckThis PR changes 1 package(s): 1 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
What this run could not seeCoarse fallback — 126 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 5547533ed98cf9e64df735a22118e8e87dd54859 && git checkout 5547533ed98cf9e64df735a22118e8e87dd54859
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 6b0be02209751420425e9194db87dfedfe6e3e38 1c85818ebc3e3af46edb6193cbda97ff9b31c3f1 && git checkout -B drift-repro 6b0be02209751420425e9194db87dfedfe6e3e38 && git merge --no-ff 1c85818ebc3e3af46edb6193cbda97ff9b31c3f1
node scripts/docs-audit/affected-docs.mjs --json 6b0be02209751420425e9194db87dfedfe6e3e38
|
Fixes #10492
Clause-②: yes — leg 2 narrows the parse-acceptance face of
InlineLocaleMapSchema(contract-review tier per the triage routing comment of 2026-08-21T02:23Z).What this does
Two legs, both in
packages/spec/src/ui/i18n.zod.ts, per the triage-ruled scope:Message accuracy (text-only). The
INLINE_LOCALE_KEYrejection message claimed the retired key-reference form (ADR-0049 enforce-or-remove: ui/widget.zod.ts 全文件 + ui/i18n.zod.ts 五个形状实测无门(#4001 批 16 改判) #5055) "resolves to nothing". Measured false — both resolvers (resolveI18nLabellimb 6, and objectui'spickLocalized, parity-pinned by objectui'si18nLabel-resolver-parity.test.ts) fall through to the first string value and return the raw dotted key, which renders as the visible label. The message now states the measured behaviour: "nothing looks the key up, so both resolvers fall through to the first string value and the raw key is rendered on screen". The doc comment carrying the same reasoning (the paragraph that argued a lonekeywas indistinguishable from a language subtag) is rewritten to record the closed boundary instead.Enforcement hole closed — the retired spellings are rejected BY NAME, in any combination.
keyis three letters, syntactically a valid BCP-47 primary subtag, so{ key: 'common.save' }alone parsed as a "languagekeyinline locale map" and would paintcommon.saveon screen; the pair form was rejected only becausedefaultValuefails the tag grammar.INLINE_LOCALE_KEYnow opens with a negative lookahead for exactly the two retired spellings — the same two names the emitted type already refuses ({ key?: never; defaultValue?: never }, [decision]InlineLocaleMap's BCP-47 key regex is erased from the emitted type, so{ key, defaultValue }type-checks on everyI18nLabelsurface in both repos — narrow the key type, or accept publish-time-only refusal? #9925 maintainer ruling 2026-08-19 option B). One mechanism (the key schema itself), no second refinement, no separate grep gate (the [decision]InlineLocaleMap's BCP-47 key regex is erased from the emitted type, so{ key, defaultValue }type-checks on everyI18nLabelsurface in both repos — narrow the key type, or accept publish-time-only refusal? #9925 ruling explicitly rejected the Option-C shape). This inherits the ADR-0049 enforce-or-remove: ui/widget.zod.ts 全文件 + ui/i18n.zod.ts 五个形状实测无门(#4001 批 16 改判) #5055 ruling; it is not a new contract question. Not a deny-list of English-looking words:deu/fra/yuestill parse (pinned).No resolution path changed:
i18n-label-resolver.tsis untouched, and the resolver probe below is byte-identical before/after — the PM's parity assumption holds (leg 2 changes parse acceptance only).Acceptance-probe table (issue's table, reproduced against BUILT src
dist/ui/index.mjs)I18nLabelSchema.safeParse({[k]:'v'}))en/zh-CN/zh-Hans-CN/default/ENkeydefaultValuenotALocale/x-private/e{ key, defaultValue }pairresolveI18nLabel({key:'common.save',defaultValue:'Save'},'en')'common.save''common.save'(resolution semantics unchanged, by design)The before-leg reproduces the issue's measurements (taken on the published 17.0.0 dist) exactly, against built src — premise verified.
Tests
i18n.test.ts(co-located): lonekey, lonedefaultValue, mixed-with-valid-locale combinations — each asserting the named error content (invalid_keyissue code + the message text naming`key`/`defaultValue`and ADR-0049 enforce-or-remove: ui/widget.zod.ts 全文件 + ui/i18n.zod.ts 五个形状实测无门(#4001 批 16 改判) #5055), not just parse failure; a message-accuracy pin (contains "first string value" / "raw key is rendered", must not contain "resolves to nothing"); a scope pin (deu/fra/yue/ENstay accepted;notALocale/x-private/estay rejected).@objectstack/specsuite at 1c85818: 416 files / 11102 tests green;pnpm --filter @objectstack/spec typecheckexit 0 (the seven [decision]InlineLocaleMap's BCP-47 key regex is erased from the emitted type, so{ key, defaultValue }type-checks on everyI18nLabelsurface in both repos — narrow the key type, or accept publish-time-only refusal? #9925 compile pins intact).srcdirectly — no dist in either leg; mutation confirmed on disk by anchored greps: old message text present 1×, new lookahead absent, before the red leg): withi18n.zod.tsrestored from origin/main tree-only, exactly the 4 new rejection/message pins go red (Tests 4 failed | 32 passed); the scope pin is direction-invariant by design and stays green in both legs. Restored from HEAD: byte-identical (git diff --exit-code HEADclean), 3 i18n test files 140/140 green.I18nLabelSchema/InlineLocaleMapSchema/resolveI18nLabel, enumerated by grep):@objectstack/service-analytics78 files / 1734 tests green;@objectstack/cli139 files / 1547 tests green — both against spec's rebuilt dist at 1c85818.Gates (all at 1c85818, clean tree; per-gate verdict lines captured, exit codes via redirect-then-read)
check:generated— "All 14 generated artifacts are up to date" (TSDoc edits moved no artifact; tree clean after full workspace build, 70/70 tasks). Dispatch-named + re-derived (node scripts/pm/dispatch-gates.mjs, no path args — 19 path-matched + 5 convention): cross-package-test-inputs · doc-formula-expressions · merge-driver · objectui-changeset · slot-lookup · spec-parsed-alias · test-source-alias · type-source-resolution · adr-0087-registration · changeset-no-major · dev-prereqs · empty-changeset · affected-docs · nul-bytes · spec empty-state / liveness / strictness-ledger / variant-docs · query-options-erasure · type-check-coverage · engine-double-contract · where-matcher — all green.check:type-check-debt: "33 ledger entr(ies) re-measured in 272.0s, 1924 raw tsc error(s) total, none above its recorded number; surplus: none".Changeset
@objectstack/specminor — FROM→TO prescription in the body; ADR-0087 disposition:not-required (already-registered ui-widget-i18n-family-retired)(the #5055 retirement's D3 record already carries the prescription; this closes its runtime enforcement gap).Generated by Claude Code