feat(spec): SpecifierSchema gains a closed valueDomain enum - #6515
Merged
Conversation
Fixes #5933. The spec half of #5712 (maintainer ruling 2026-08-06, reading 1); the ruling comment pre-authorised this contract-first split, and the enforcement half stays in `service-settings` (#5712, Blocked-by this). ## Premise, re-verified against origin/main (1818998) All three of the issue's claims hold, measured here rather than taken on trust: - `SpecifierSchema`'s value-constraint slots are exactly `options` / `pattern` / `min` / `max` / `minLength` / `maxLength` (settings-manifest.zod.ts). There is no slot that can say "a published standard is the boundary", and an undeclared key is stripped by Zod rather than refused. - The pull is real and larger than the issue states: `localization.manifest.ts` needs it on `timezone` (:34), `currency` (:114) and `default_country` (:67), and `company.manifest.ts:55` carries the *same* `^[A-Za-z]{2}$` country pattern with the same hole. Four keys, two manifests. - The Intl traps reproduce exactly on this container (Node v22.22.2): `Intl.supportedValuesOf('timeZone')` returns 418 entries and omits `UTC`, `Asia/Kolkata`, `Europe/Kyiv`, `Asia/Ho_Chi_Minh`, `US/Eastern` and `GMT`; the `Intl.DateTimeFormat` probe accepts all six and rejects `Mars/Olympus`; `Intl.supportedValuesOf('currency')` returns 162, holds `CHF` and all nine curated codes, and does not hold `XYZ`. The named precedents are where the issue says: `isValidTimeZone` at `packages/core/src/security/resolve-authz-context.ts:449`, and the IANA-probe assertion in `localization.manifest.test.ts`. ## What lands `SpecifierValueDomainSchema` — a closed enum — plus an optional `specifier.valueDomain` key that references it. Declaring it moves the enforcement boundary onto the standard's membership and degrades `options` to a UI convenience list; omitting it changes nothing, so #5131's exhaustive-options semantics stay intact for registry-backed tables (`mail.provider`, `sms.provider`). `pattern` / `minLength` / `maxLength` still apply alongside a domain: shape and membership narrow independently and a value must satisfy both. One parse rule is added: a layout-only specifier carries no value, so it may not declare a `valueDomain`. Without it the key would be a silent no-op on a `group` — declared and never enforced, the shape Prime Directive #10 refuses. No enforcement ships here (Prime Directive #2): spec declares, `service-settings` executes. ## Vocabulary: three members, not the proposal's four The dispatch left the final vocabulary to this seat. `iana_time_zone`, `iso_4217_currency` and `iso_3166_alpha2` are in — each has a metadata key that needs it today (above). `bcp47_locale` is deliberately OUT, on two measured grounds: - Its only candidate key is `localization.locale`, a `select` whose four options ARE the shipped message catalogs — a registry-backed table. Declaring a domain there would LOOSEN it, admitting locales the platform has no catalog for. That is a regression, not the fix the issue asks for. - BCP-47 has no membership registry to enforce against: `Intl.getCanonicalLocales('xx-YY')` succeeds (measured). The "domain" would only re-check syntax — precisely the weakness `pattern` already has and this key exists to fix, so it would buy zero rejection power. Startup-focus reading: a fourth member with no pull and no enforceable definition is vocabulary expansion for its own sake. The enum is closed, so it is refused by name rather than silently accepted, and it can return the day a key needs it. ## The membership definitions are pinned, not just documented Both halves have to agree on WHAT each domain is, and for two of the three the obvious oracle is the wrong one. The TSDoc states each definition and a test block measures it, so the doc goes red rather than rotting: - `iana_time_zone` = the `Intl.DateTimeFormat` probe. NOT `supportedValuesOf` — which not only omits values this platform ships but *renames* them (`Asia/Calcutta`, `Europe/Kiev`), so even a normalising membership test is not free. - `iso_4217_currency` = `Intl.supportedValuesOf('currency')`. Known gaps (`VED`, the metal/fund codes) are named in the TSDoc so widening is a decision rather than a surprise. - `iso_3166_alpha2` has NO standard-library oracle, and this is the finding most worth carrying to #5712: the tempting test — "the `Intl.DisplayNames` region name differs from the input" — admits `ZZ` (it maps to "Unknown Region", and `ZZ` is the exact value #5933 cites as slipping past `^[A-Za-z]{2}$`) and admits `UK` (a CLDR alias that is not an ISO 3166-1 code). The enforcing side must carry an explicit alpha-2 list. That list does not belong in spec. ## Reverse verification (direction predicted first) Predicted RED, and red it is: deleting the `valueDomain` limb from the schema turns 4 of the 6 `Specifier.valueDomain` cases red, headed by "SURVIVES the parse" — which is #5712's own `format` measurement inverted (Zod strips the undeclared key, `parsed.valueDomain` is `undefined`). "rejects an unknown domain" also goes red, i.e. without the limb a misspelt domain is silently swallowed rather than refused. The 2 that stay green stay green honestly: "is optional" and "a select still requires options" do not depend on the limb. The schema file was restored byte-identical afterwards (verified with diff). ## Verification (all foreground, real readings) - `pnpm --filter @objectstack/spec test` -> 340 files, **8723 passed**, 0 failed - targeted verbose run of `settings-manifest.test.ts` + `type-alias-convention.pin.test.ts` -> **46 passed**, including all 13 new cases - `pnpm --filter @objectstack/spec exec tsc --noEmit` -> clean - `pnpm --filter @objectstack/spec typecheck` -> clean (test layer compiles; debt ledger unchanged at 58 files / 267 errors) - `turbo run typecheck --filter=./packages/* --filter=./packages/*/*` -> **119 successful, 119 total** - `pnpm --filter @objectstack/spec check:generated` -> **10/10 up to date** (api-surface and content/docs/references were stale and regenerated with `--fix`, exactly the 2 it proved stale; authorable-surface auto-recorded `system/Specifier:valueDomain`) - `pnpm --filter @objectstack/spec gen:openapi` -> ran (ungated generator); writes only the gitignored `json-schema/openapi.json`, no tracked diff - `pnpm lint` -> clean - `pnpm check:spec-parsed-alias` -> "1465 bare z.input aliases, **755 pinned isomorphic**, 710 paired with an XParsed. OK" - `check:nul-bytes` / `check:empty-changeset` / `check:adr-0087-registration` / `check:quick-reference-counts` / `check:doc-authoring` / `check:role-word` / `check:adr-anchors` / `check:release-notes` / `check:published-files` / `check:exported-any` / `check:dual-source-exports` / `check:liveness` / `check:generated --reconcile-only` -> all green - `pnpm --filter @objectstack/service-settings test` -> 15 files, **269 passed** (nearest consumer, untouched by this change; the package declares no `typecheck` script) `check:adr-0087-registration` confirms this is not a declared-breaking changeset — the change is purely additive, so no disposition marker is owed. ## ADR-0122 `SpecifierValueDomainSchema` is a `z.enum`: no default, no transform, so `z.input` and `z.infer` coincide. Per the ADR it gets a pin (`Iso758`) rather than a permanent-synonym `SpecifierValueDomainParsed`, and the registry's hardcoded count moves 754 -> 755 with its receipt comment recorded at the count case, in the file's own idiom. ## Ruling mapping | Dispatch clause | Where it lands | |:---|:---| | optional closed `valueDomain` enum | `settings-manifest.zod.ts` — `SpecifierValueDomainSchema` + the `valueDomain` key | | semantics in TSDoc, `options` degrades to UI list | the key's TSDoc; pinned by the "still requires options" and "is optional" cases | | undeclared = unchanged (#5131) | pinned by "is optional — keeps #5131 exhaustive-options semantics" | | enforcement stays in service-settings | nothing executes here; stated in the TSDoc and the changeset | | timezone = `Intl.DateTimeFormat` probe, not `supportedValuesOf` | TSDoc + two pin cases | | currency = `supportedValuesOf('currency')` | TSDoc + one pin case | | final vocabulary decided by this seat, with reasons | three members; `bcp47_locale` dropped, reasons above and in the changeset | | schema + authorable-surface baseline + pins + docs + minor changeset | all present | | do not ride #5932 | not touched | ## Out-of-scope findings (recorded, not fixed) 1. `type-alias-convention.pin.test.ts`'s count case is titled "still declares all 751 isomorphic pins" while asserting 754 (now 755). The title has been stale since #6037 moved 751 -> 754 without updating it. Left alone deliberately — it predates this change and a title edit is not this issue's scope — but it now reads two moves behind, so it is worth a one-line docs fix. 2. `pattern` is documented "(text only)" at `settings-manifest.zod.ts` yet no parse rule scopes it to `text`; the same is true of `rows`, `min`/`max` and the length bounds. `valueDomain` follows that same house style (documented applicable types, only the layout-only case refused) rather than inventing a stricter convention for one key. If the family should be type-scoped, that is one deliberate change across all of them, not a rider here. Distinct from #5932, which is about those bounds being inert on the *write path*. ## 交付摘要(PM 开 PR 素材) - **前提**:对 origin/main 1818998 逐条复核成立,且拉动比 issue 所述更强 —— 四个键、两个 manifest(`localization` 的 timezone/currency/default_country + `company.country` 同一个 `^[A-Za-z]{2}$` 洞)。Node v22.22.2 上三条 Intl 实测读数与 issue 完全一致。 - **落地**:spec 只声明不执行 —— 新增闭合枚举 `SpecifierValueDomainSchema` 与可选键 `specifier.valueDomain`,外加一条 parse 规则(布局型 specifier 不得声明)。 - **词表定形**:三员,砍掉提案里的 `bcp47_locale`。两条实测理由:它唯一的候选键 `localization.locale` 的 options 就是随包发的语言包(注册表背书表),声明域反而会放松它;且 BCP-47 没有成员登记表可校验(`Intl.getCanonicalLocales('xx-YY')` 通过),那个「域」只会退化成又一次语法检查 —— 正是本键要修的 `pattern` 短板。 - **给 #5712 的关键交接**:`iso_3166_alpha2` 没有标准库 oracle。`Intl.DisplayNames` 的「显示名与输入不同」判据会放行 `ZZ`(映射为 "Unknown Region",正是本单点名要拒的值)和 `UK`(CLDR 别名,并非 ISO 3166-1 码)。执行半边必须自带 alpha-2 码表,码表不进 spec。 - **反向验证**:先预测后跑,方向为红 —— 摘掉 `valueDomain` 肢体后 6 个用例红 4,领头的正是 #5712 那次 `format` 被静默剥掉的实测反过来写。另 2 个诚实地保持绿(不依赖该肢体)。schema 文件已 diff 验证还原为逐字节一致。 - **验证读数**:spec 8723 测试全绿、`turbo typecheck` 119/119、生成物 10/10、`check:spec-parsed-alias` 报 755 pinned、lint 与门禁族全绿、service-settings 269 测试全绿。 - **界外发现**两条见上(pin 计数用例标题过时;`pattern` 族的类型作用域「声明而不强制」),均未夹带。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011btrhv6sHn6JkN93YRtGQp
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
qq9340100
enabled auto-merge
August 8, 2026 02:58
Contributor
📓 Docs Drift CheckThis PR changes 1 package(s): 112 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
|
This was referenced Aug 8, 2026
This was referenced Aug 8, 2026
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.
Fixes #5933
#5712 裁读法 1 的 spec 半边(裁决评论预授权 contract-first 拆分);执行半边留
service-settings(#5712 Blocked-by 本单)。前提复核(对 origin/main
1818998实测)SpecifierSchema值约束位确仅 options/pattern/min/max/minLength/maxLength,无「标准域为边界」的声明位,未声明键被 Zod 剥除而非拒绝;localization.manifest的timezone/currency/default_country+company.manifest:55同一个^[A-Za-z]{2}$洞——四键两 manifest;supportedValuesOf('timeZone')418 项缺 UTC/Asia/Kolkata 等且改名 Asia/Calcutta;DateTimeFormat 探针六值全收、拒 Mars/Olympus;currency 162 项含 CHF 与九个在库值)。落地
SpecifierValueDomainSchema(闭合枚举)+ 可选specifier.valueDomain:声明时标准域为执行边界、options退化为 UI 便利列表;未声明一切照旧(#5131 穷尽语义对注册表背书表不变);pattern/长度约束与域独立并行收窄。新增一条 parse 规则:layout-only specifier 不得声明valueDomain(否则是声明即空转,PD #10 拒绝的形状)。零执行落地(PD #2:spec 声明,service-settings 执行)。词表:三成员,非提案的四
派发词把词表定形授权本席:
iana_time_zone/iso_4217_currency/iso_3166_alpha2各有今天就需要它的键;bcp47_locale刻意排除,两条实测理由——唯一候选键localization.locale是注册表背书的 select(声明域反而放宽,属倒退);BCP-47 无成员注册表可执行(Intl.getCanonicalLocales('xx-YY')实测通过),只能复查语法 = pattern 已有的弱点。枚举闭合,回头需要时按名恢复。成员定义已钉(非仅文档)
TSDoc 写定义、测试块测量之,文档腐烂即转红:时区 = DateTimeFormat 探针(⛔ 非 supportedValuesOf);币种 =
supportedValuesOf('currency')(已知缺口 VED/金属基金码点名在 TSDoc);alpha2 无标准库 oracle——Intl.DisplayNames判 region 名会放行ZZ(Unknown Region)与UK(CLDR 别名非 ISO 码),执行侧须带显式清单,该发现已在正文标注供 #5712 消费。反向验证(方向先写后跑)
删除 schema 的
valueDomain臂 → 预判并实测 6 case 中 4 红(领头正是 #5712 自己那次format测量的倒置:键被剥、parsed.valueDomain === undefined),2 绿者诚实(不依赖该臂);复原后 diff 字节一致。验证(全前台实测读数)
spec 340 文件 8723 全过;定向 46 过(含 13 新 case);tsc/typecheck 干净(债务台账 58/267 未增);全仓 turbo typecheck 119/119;
check:generated10/10(api-surface 与 references 恰为重生成的 2 项,authorable-surface 自动记录system/Specifier:valueDomain);check:spec-parsed-alias755 pinned OK;lint 与门禁族全绿。changeset(spec minor)。交付通道注记
云端工头 B 会话(
session_011btrhv6sHn6JkN93YRtGQp,串行卡 1/2)无 GitHub 工具,走交付降级通道:dev 实现并 push 分支,PM(session_011M7UwH25Unfi73UHim7ajY)代开本 PR 并跟进 CI 至合并。Generated by Claude Code