Skip to content

feat(spec): SpecifierSchema gains a closed valueDomain enum - #6515

Merged
qq9340100 merged 1 commit into
mainfrom
claude/issue-5933-specifier-value-domain
Aug 8, 2026
Merged

feat(spec): SpecifierSchema gains a closed valueDomain enum#6515
qq9340100 merged 1 commit into
mainfrom
claude/issue-5933-specifier-value-domain

Conversation

@qq9340100

Copy link
Copy Markdown
Collaborator

Fixes #5933

#5712 裁读法 1 的 spec 半边(裁决评论预授权 contract-first 拆分);执行半边留 service-settings(#5712 Blocked-by 本单)。

前提复核(对 origin/main 1818998 实测)

  • SpecifierSchema 值约束位确仅 options/pattern/min/max/minLength/maxLength,无「标准域为边界」的声明位,未声明键被 Zod 剥除而非拒绝;
  • 拉动比 issue 更大:localization.manifesttimezone/currency/default_country + company.manifest:55 同一个 ^[A-Za-z]{2}$ 洞——四键两 manifest;
  • Intl 陷阱在本容器逐条复现(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:generated 10/10(api-surface 与 references 恰为重生成的 2 项,authorable-surface 自动记录 system/Specifier:valueDomain);check:spec-parsed-alias 755 pinned OK;lint 与门禁族全绿。changeset(spec minor)。

交付通道注记

云端工头 B 会话(session_011btrhv6sHn6JkN93YRtGQp,串行卡 1/2)无 GitHub 工具,走交付降级通道:dev 实现并 push 分支,PM(session_011M7UwH25Unfi73UHim7ajY)代开本 PR 并跟进 CI 至合并。


Generated by Claude Code

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
@vercel

vercel Bot commented Aug 8, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
objectstack Ignored Ignored Aug 8, 2026 2:58am

Request Review

@qq9340100
qq9340100 enabled auto-merge August 8, 2026 02:58
@github-actions

github-actions Bot commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/spec.

112 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:

  • content/docs/ai/agents.mdx (via @objectstack/spec)
  • content/docs/ai/skills-reference.mdx (via @objectstack/spec)
  • content/docs/ai/skills.mdx (via @objectstack/spec)
  • content/docs/api/client-sdk.mdx (via @objectstack/spec)
  • content/docs/api/environment-routing.mdx (via @objectstack/spec)
  • content/docs/api/error-catalog.mdx (via @objectstack/spec)
  • content/docs/api/error-handling-client.mdx (via @objectstack/spec)
  • content/docs/api/error-handling-server.mdx (via @objectstack/spec)
  • content/docs/api/index.mdx (via @objectstack/spec)
  • content/docs/automation/approvals.mdx (via @objectstack/spec)
  • content/docs/automation/connectors.mdx (via @objectstack/spec)
  • content/docs/automation/flows.mdx (via @objectstack/spec)
  • content/docs/automation/hook-bodies.mdx (via packages/spec)
  • content/docs/automation/hooks.mdx (via @objectstack/spec)
  • content/docs/automation/index.mdx (via @objectstack/spec)
  • content/docs/automation/webhooks.mdx (via @objectstack/spec)
  • content/docs/automation/workflows.mdx (via @objectstack/spec)
  • content/docs/concepts/architecture.mdx (via @objectstack/spec)
  • content/docs/concepts/design-principles.mdx (via packages/spec)
  • content/docs/concepts/index.mdx (via @objectstack/spec)
  • content/docs/concepts/metadata-driven.mdx (via @objectstack/spec)
  • content/docs/concepts/metadata-lifecycle.mdx (via packages/spec)
  • content/docs/concepts/north-star.mdx (via @objectstack/spec)
  • content/docs/data-modeling/analytics.mdx (via @objectstack/spec)
  • content/docs/data-modeling/drivers.mdx (via @objectstack/spec)
  • content/docs/data-modeling/external-datasources.mdx (via @objectstack/spec)
  • content/docs/data-modeling/field-types.mdx (via @objectstack/spec)
  • content/docs/data-modeling/fields.mdx (via @objectstack/spec)
  • content/docs/data-modeling/formulas.mdx (via @objectstack/spec)
  • content/docs/data-modeling/index.mdx (via @objectstack/spec)
  • content/docs/data-modeling/objects.mdx (via @objectstack/spec)
  • content/docs/data-modeling/queries.mdx (via @objectstack/spec)
  • content/docs/data-modeling/schema-design.mdx (via @objectstack/spec)
  • content/docs/data-modeling/seed-data.mdx (via @objectstack/spec)
  • content/docs/data-modeling/validation-rules.mdx (via @objectstack/spec)
  • content/docs/data-modeling/validation.mdx (via @objectstack/spec)
  • content/docs/deployment/cli.mdx (via @objectstack/spec)
  • content/docs/deployment/tenancy-modes.mdx (via @objectstack/spec)
  • content/docs/deployment/troubleshooting.mdx (via @objectstack/spec)
  • content/docs/deployment/validating-metadata.mdx (via @objectstack/spec)
  • content/docs/getting-started/build-with-claude-code.mdx (via @objectstack/spec)
  • content/docs/getting-started/common-patterns.mdx (via @objectstack/spec)
  • content/docs/getting-started/examples.mdx (via @objectstack/spec)
  • content/docs/getting-started/quick-reference.mdx (via @objectstack/spec)
  • content/docs/getting-started/quick-start.mdx (via @objectstack/spec)
  • content/docs/getting-started/your-first-project.mdx (via @objectstack/spec)
  • content/docs/kernel/cluster.mdx (via @objectstack/spec)
  • content/docs/kernel/contracts/auth-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/cache-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/data-engine.mdx (via @objectstack/spec)
  • content/docs/kernel/contracts/index.mdx (via @objectstack/spec)
  • content/docs/kernel/contracts/metadata-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/storage-service.mdx (via @objectstack/spec)
  • content/docs/kernel/index.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/data-service.mdx (via @objectstack/spec)
  • content/docs/kernel/runtime-services/email-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/examples.mdx (via @objectstack/spec)
  • content/docs/kernel/runtime-services/index.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/queue-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/sharing-service.mdx (via @objectstack/spec)
  • content/docs/kernel/runtime-services/sms-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/storage-service.mdx (via @objectstack/spec)
  • content/docs/kernel/services-checklist.mdx (via @objectstack/spec)
  • content/docs/kernel/services.mdx (via @objectstack/spec)
  • content/docs/permissions/authorization.mdx (via @objectstack/spec)
  • content/docs/permissions/permission-sets.mdx (via @objectstack/spec)
  • content/docs/permissions/permissions-matrix.mdx (via @objectstack/spec)
  • content/docs/permissions/positions.mdx (via @objectstack/spec)
  • content/docs/permissions/rls.mdx (via @objectstack/spec)
  • content/docs/permissions/sharing-rules.mdx (via @objectstack/spec)
  • content/docs/plugins/adding-a-metadata-type.mdx (via @objectstack/spec)
  • content/docs/plugins/development.mdx (via @objectstack/spec)
  • content/docs/plugins/index.mdx (via @objectstack/spec)
  • content/docs/plugins/packages.mdx (via @objectstack/spec)
  • content/docs/protocol/backward-compatibility.mdx (via @objectstack/spec)
  • content/docs/protocol/diagram.mdx (via packages/spec)
  • content/docs/protocol/kernel/config-resolution.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/http-protocol.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/i18n-standard.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/index.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/lifecycle.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/plugin-spec.mdx (via @objectstack/spec)
  • content/docs/protocol/knowledge.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/index.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/query-syntax.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/schema.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/security.mdx (via packages/spec)
  • content/docs/protocol/objectql/state-machine.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/actions.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/concept.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/index.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/layout-dsl.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/record-alert.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/widget-contract.mdx (via @objectstack/spec)
  • content/docs/releases/implementation-status.mdx (via @objectstack/spec)
  • content/docs/releases/index.mdx (via @objectstack/spec)
  • content/docs/releases/v12.mdx (via @objectstack/spec)
  • content/docs/releases/v13.mdx (via @objectstack/spec)
  • content/docs/releases/v16.mdx (via @objectstack/spec)
  • content/docs/releases/v17.mdx (via @objectstack/spec)
  • content/docs/releases/v9.mdx (via @objectstack/spec)
  • content/docs/ui/actions.mdx (via @objectstack/spec)
  • content/docs/ui/apps.mdx (via @objectstack/spec)
  • content/docs/ui/create-vs-edit-form.mdx (via @objectstack/spec)
  • content/docs/ui/dashboards.mdx (via @objectstack/spec)
  • content/docs/ui/field-grouping-and-order.mdx (via @objectstack/spec)
  • content/docs/ui/forms.mdx (via @objectstack/spec)
  • content/docs/ui/index.mdx (via @objectstack/spec)
  • content/docs/ui/public-data-collection.mdx (via @objectstack/spec)
  • content/docs/ui/setup-app.mdx (via @objectstack/spec)
  • content/docs/ui/translations.mdx (via @objectstack/spec)
  • content/docs/ui/views.mdx (via @objectstack/spec)

Advisory only. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs origin/main → pass the list as args.docs.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation protocol:system size/m tests tooling

Projects

None yet

2 participants