fix(service-settings): localization 的标准域是执行边界 —— valueDomain 两扇门落地 (#5712) - #6581
Merged
os-project-manager merged 1 commit intoAug 8, 2026
Merged
Conversation
…rcement boundary — valueDomain enforced on both doors (#5712) The 2026-08-06 ruling (reading 1): the curated options tables on localization.timezone / localization.currency are UI convenience lists; the enforcement boundary is the standard domain. The spec half shipped as #6515 (SpecifierValueDomainSchema); this is the services half. - manifest: timezone declares iana_time_zone, currency iso_4217_currency, default_country iso_3166_alpha2 (third case of the same hole — ZZ passed ^[A-Za-z]{2}$) - both doors judge a declared domain at their one decision point: validatePatch (after pattern — shape first, membership second) and effectiveEnvOverride (loud error + fallback, #5204 contract unchanged) - membership follows the spec's pinned definitions: DateTimeFormat probe / supportedValuesOf('currency') / explicit 249-code alpha-2 list - a specifier without valueDomain is byte-for-byte unchanged (#5131 exhaustive options), pinned by regression tests on both doors - breach code is invalid_value with constraint { valueDomain } (ADR-0114 slot for a breach no member names — the #6199 precedent); invalid_option would misname the set consulted Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01USNUyHEr7uaU6MoEWXitei
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
Contributor
📓 Docs Drift CheckThis PR changes 1 package(s): 7 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
|
os-project-manager
marked this pull request as ready for review
August 8, 2026 05:52
os-project-manager
enabled auto-merge
August 8, 2026 05:52
os-project-manager
deleted the
claude/issue-5712-localization-valuedomain
branch
August 8, 2026 06:05
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #5712. The services half of the 2026-08-06 ruling (reading 1): the curated
optionstables onlocalization.timezone/localization.currencyare UI convenience lists; the enforcement boundary is the standard domain (IANA / ISO 4217). The spec half shipped as #6515 (2fdb36e,SpecifierValueDomainSchema); this PR makesservice-settingsenforce what that vocabulary declares.Merged-ref re-verify (first action, per the #6465 discipline)
Re-read at post-merge
origin/main(1a53a02) before any code:valueDomainenum #6515 delivered a three-member closed enum —iana_time_zone,iso_4217_currency,iso_3166_alpha2. The issue's proposed fourth memberbcp47_localedid NOT survive (deliberately rejected:localization.locale's options ARE the shipped catalogs — a registry-backed table that a domain would loosen — and BCP-47 has no membership registry to enforce against). No premise of this card depended on it:localestays undeclared and exhaustive.iso_3166_alpha2has no standard-library oracle — the enforcing side must carry the alpha-2 list, and the list must not live in spec.stepgrid on the settings write path and the env path (#6199) #6501 (stepgrid, merged the same day) had already reshaped this validation surface: the constraint families are judged by shared helpers at ONE decision point per door (firstRejectedOption/firstRangeViolation, both called fromvalidatePatchandeffectiveEnvOverride). This PR follows that exact shape — one new shared judgment (firstRejectedDomainMember), called from both doors, and the test blocks mirror fix(service-settings): enforce the declaredstepgrid on the settings write path and the env path (#6199) #6501's idiom.valueDomainkeys surviveSettingsManifestSchema.parse(the localization 的 timezone / currency 用「策展式 options 表」当穷尽执行边界:合法的 IANA 时区与 ISO 4217 币种被拒 #5712 dev report'sformat-stripped measurement, now inverted by the closed enum).What lands
Manifest (
localization.manifest.ts):timezonedeclaresiana_time_zone,currencydeclaresiso_4217_currency, anddefault_countrydeclaresiso_3166_alpha2. Options stay curated; descriptions are untouched — they promised these open domains all along, and the declaration is what makes the promise true.Both doors (
settings-service.ts), each at its existing single decision point:validatePatch): a domain-bearing specifier skips the exhaustive-options check and judges the standard's membership instead. Ordering:patternspeaks first (shape and membership narrow independently, a value must satisfy both; the shape breach is the coarser fact — the same one-error-per-key argument as settings 的step是同族第五个声明但不执行的值约束键 —— 先要一个「它算不算值约束」的判断 #6199's window-before-grid). Breach code isinvalid_valuewithconstraint: { valueDomain: '…' }.effectiveEnvOverride): same membership judgment. A garbage override is loudly reported once and ignored — falls to the next cascade layer, pins nothing (env 来源的 settings 值绕过 manifest 的 options 表校验 —— #5094 在写入 API 上堵住的洞,在 OS_* 覆盖这一侧原样敞开 #5204's contract, unchanged). A legal one wins the cascade and locks the key, soOS_LOCALIZATION_TIMEZONE=Europe/Zurichis now honored.Error code —
invalid_value, flagged as a deliberate choice. NoFieldErrorCodemember names a standard-domain breach.invalid_optionwas considered and rejected: its catalog line reads "not a member of the field's declared options", and with a domain declared, the declared options are exactly the list a legal value may be outside of — the code would misname the set consulted.invalid_valueis the catalog's declared slot for "rejected for a reason no other member names", the same verdict #6199 reached for the step grid. The domain travels inconstraint, so clients can branch without parsing prose. HTTP surface unchanged: 400SETTINGS_VALIDATIONwithdetails.fields.Validator definitions (
value-domains.ts, new — follows the spec's pinned TSDoc):iana_time_zone= theIntl.DateTimeFormatprobe (theisValidTimeZoneshape fromresolve-authz-context.ts, restated because it is module-private there). ⛔ NOTIntl.supportedValuesOf('timeZone'): measured on Node 22 it is a 418-entry CLDR subset that omitsUTC(the manifest's own default),Asia/Kolkata(a curated option),Europe/Kyiv,US/Eastern,GMT— and renames rather than merely omits (Asia/Calcutta,Europe/Kiev), so using it would replay this card's defect at 418 entries.iso_4217_currency=Intl.supportedValuesOf('currency')(measured usable: 162 entries, CHF in, all 9 curated in, XYZ out), cached once.iso_3166_alpha2= an explicit list of the 249 officially assigned codes, carried here per the spec handoff (Intl.DisplayNamesnamesZZ"Unknown Region" and admits theUKalias, so it is not an oracle). Structural pins: size 249, all uppercase pairs;ZZ/UK/XXrefused.default_country: adopted in this PR. The merged enum covers ISO 3166-1 alpha-2, so the third case of the same hole is closed here:ZZ(shape-valid, assigned to nobody) is refused by membership whilepatternstill owns the shape verdict (ZZZ→invalid_format). One deliberate tightening to note: membership is exact uppercase, as the standard spells its codes —uswas previously accepted by^[A-Za-z]{2}$and is now refused. No in-repo consumer writes lowercase (measured: the key has no runtime reader in-repo).Regression pin (#5131 semantics survive): a specifier WITHOUT
valueDomainis byte-for-byte unchanged — pinned on both doors againstmail.provider(sendgridstill refusedinvalid_option) and localization's own domain-less keys (first_day_of_week,date_formatenv override).Reverse verification (direction predicted first)
Predicted: reverting
settings-service.tstoorigin/main(manifest keeps its declarations, which the old service ignores) turns the new cases red in two distinct ways — acceptance cases red viainvalid_optionrefusals, theZZrejection case red by resolving — while the #5131 regression pins and the validator unit pins stay honestly green. Measured: 15 of the 18 new service/route cases went red exactly as predicted, and the 4 survivors are precisely the pins that must not depend on the new limb ("still accepts every curated option", the two #5131 exhaustive pins, the typo-fallback pin). The fixed file was restored and verified byte-identical (diff), then the full suite re-run green.Verification (all foreground, real readings)
pnpm --filter @objectstack/service-settings test→ 16 files, 318 passed, 0 failed (49 new cases across 4 files)pnpm exec tsc --noEmit(in the package) → 13 errors, all pre-existing — exactly the ledgered debt count incheck-type-check-coverage.mjs; my files contribute 0. (Two ES2022-lib traps were fixed rather than ledgered:Object.hasOwnand typedsupportedValuesOfare spelled ES2020-compatibly with removal notes, since the rootlibbump is not this PR's decision.)pnpm exec eslinton all 7 touched files → cleannode scripts/check-nul-bytes.mjs→ OK; control-byte self-scan of new files → clean.changeset/localization-value-domain-enforced.md(patch,@objectstack/service-settings)Out of scope, filed separately
^[A-Za-z]{2}$收下ZZ,而iso_3166_alpha2域执行已在库 #6579 —company.manifest.ts'scountrykey carries the same^[A-Za-z]{2}$hole (feat(spec): SpecifierSchema gains a closedvalueDomainenum #6515's premise re-verify named it as the fourth key); now that enforcement exists, adoptingiso_3166_alpha2there is a one-line follow-up (filed withBlocked-by: #5712).pattern:写入门拒的形状非法值,OS_*覆盖照收——#5204 不对称的最后一族 #6580 —effectiveEnvOverridejudges options / domains / bounds but notpattern: apattern-only key accepts via env what the write door refuses — the env 来源的 settings 值绕过 manifest 的 options 表校验 —— #5094 在写入 API 上堵住的洞,在 OS_* 覆盖这一侧原样敞开 #5204 asymmetry for the one remaining declared-constraint family (in-repo instance:OS_COMPANY_COUNTRY=ZZZ).🤖 Generated with Claude Code
https://claude.ai/code/session_01USNUyHEr7uaU6MoEWXitei
Generated by Claude Code