Repository navigation
No declarative way to constrain an object FIELD to a value domain (iana_time_zone), and neither extension point an app can reach can express it #14168
Description
Activity
- added a commit that references this issue
on Sep 1, 2026 Triage (R+89, triage seat, session
session_019kDRpB7D2XzVzkaLp57T5D): graded →needs-user-decision·priority:p2·domain:spec· type Feature.findingremoved. Reason for the inbox: a new declarative slot on a published field schema is a feature / contract-shape addition — the human floor, never adjudicated by a seat. The build-time sandbox-lowering hazard the card also documents is a defect in its own right and is split into a separate card (filed by this seat right after this comment; see the next comment for its number). 决策分析(中文,按四维模板):<!-- os-decision-facets -->
一句话问题:应用作者想把一个字段限定为「真实存在的 IANA 时区」,平台今天没有任何声明式写法能做到;而最自然的替代写法(内联钩子)会通过全部本地门禁、在生产环境把该对象的每一次写入都拒掉。前提(每条带 re-check):
- 前提 1:值域词表与成员判据已存在,但只从设置 Specifier 可达 ——
git grep -n "iana_time_zone" origin/main -- packages/spec/src(阳性对照:同文件iso_4217_currency非 0)。 - 前提 2:
FieldSchema无 valueDomain/format 槽 ——git grep -n "valueDomain" origin/main -- packages/spec/src/data/field.zod.ts应为 0(对照maxLength非 0)。 - 前提 3:QuickJS 沙箱无
Intl—— 立卡人在 17.2.0 实测(typeof Intl 为 undefined),本席未复测。
选项 × 真实代价:
选项 做什么 客户可感知的代价 A 字段级 valueDomain,复用 Specifier 的同一闭合词表(现有三值)与同一成员判据,写路径响亮拒绝新增一个已声明键 = 永久维护义务;但词表与判据已在,新增的是一个槽位不是一套机制;Studio / OpenAPI / 表单自动看到 B validations[].format词表加入同一批成员与 A 同源;但 format今天是 email/url/phone/json 的形状校验,把「成员判据靠宿主 Intl」混进去,让 format 一半可离线判、一半不能C 不加槽位,只在 hook.zod.ts写明 body 拿不到宿主内建、字符串 handler 是唯一路线零契约扩张;但「弃用」方向与「唯一可行路线」的矛盾原样留给每个作者;时区/币种/国家码字段仍不可声明 业务含义直译:A = 像 Salesforce 的 picklist 受全局值集约束,错值在保存时被拒;B = 像给字段挂一个 format=timezone;C = 告诉作者「自己写代码,但别用我们推荐的写法」。
四轴(业务立场;①权重 ≥50%):
① 项目长远合理性:A 缩小特例 —— 设置与字段共用一个值域词表、一个成员判据,而不是让每个应用各自冻结一份 tzdata 快照进元数据;C 是把平台缺口外包给每个应用重新发明。⇒ 荐 A。
② 实际业务拉动:有 —— objectstack-ai/duly#24 是真实下游应用,今天就在用字符串 handler 绕行;时区 / 币种 / 国家码是任何 SaaS 都会有的字段,不是投机面。
③ 防 AI 犯错:A 让错值在保存时响亮拒绝、且是可翻译的声明式消息;C 之下 AI 最自然写出的内联钩子会通过 validate / typecheck / test / build,到生产才变成「拒绝一切写入」—— 最坏的静默形状。
④ 创业阶段不扩散:A 只加一个字段级槽位并复用既有三值词表,⛔ 不扩词表、不加机制;「给沙箱加 Intl」不在选项内(那是能力扩张)。推荐:A(字段级
valueDomain,闭合词表 = 现有三值),回退 C(只改文案)。置信缺口:本席未复测沙箱无 Intl;未测 Studio / 表单层接入成本;format词表与valueDomain是否同批统一,留给 spec 席实施时判断。裁后执行(你不用管):A ⇒ 同笔转
pm:queue+domain:spec,派发认领 Clause-② yes(扩公开面,契约复审档);构建期沙箱诊断的拆卡独立入队。C ⇒ 转pm:queue文档卡 p3。
Generated by Claude Code
- 前提 1:值域词表与成员判据已存在,但只从设置 Specifier 可达 ——
Split filed: #14301 carries the build-time sandbox-lowering defect (the
GLOBALSallowlist inpackages/cli/src/utils/detect-free-identifiers.tsadmitsIntl, which the QuickJS sandbox does not provide), graded directly topm:queue·domain:cli· p2. This card stays in the decision inbox for the feature question only.
Generated by Claude Code
os-project-manager commented
on Sep 2, 2026 CollaboratorMore actionsMaintainer ruling recorded — A: field-level
valueDomain, closed vocabulary = the existing three valuesDirector seat, summon #8, session
session_01ShyhexkB2d1AeRZ85tgAAe, 2026-09-02.Provenance (who / verbatim / where): maintainer, live PM chat with the director seat, 2026-09-02, replying to decision batch #3 in which this card was item 2 with the recommendation A (fallback C). Verbatim reply: 「同意」.
Ruled: A.
FieldSchemagains avalueDomainslot whose vocabulary is exactlySpecifierValueDomainSchema's current three members (iana_time_zone·iso_4217_currency·iso_3166_alpha2) — one closed vocabulary and one membership predicate shared by settings specifiers and object fields; ⛔ the vocabulary does not widen in this card. Enforced on the write path with a loud, translatable refusal; visible to Studio, OpenAPI and the form layer through the schema. Option B (aformatmember) is not taken —formatstays the offline shape-check family. Option C (prose only) is not taken. AddingIntlto the sandbox is out of scope; #14301 (the build-time lowering hazard) proceeds independently.Execution:
domain:speclane;Clause-②: yes(a published field schema widens its accept set), dispatch atCONTRACT_REVIEW_TIER. #14238 (ruled A in the same batch) is the first consumer: its two timezone columns declare this domain, so the two cards sequence spec-first.State transition, same stroke:
needs-user-decision→pm:queue. Ledger: director seat post #12708, summon #8.
Generated by Claude Code
Claim: PM loop,
domain:specseat (seat post #6017), R5 of this shift — implementing the maintainer's ruling A (comment 5507503059, 「同意」 2026-09-02):FieldSchemagains avalueDomainslot whose vocabulary is exactlySpecifierValueDomainSchema's three members, one closed vocabulary shared by settings specifiers and object fields; the write-path enforcement (engine) and the settings re-point onto the shared predicate are follow-up cards this seat files at ACCEPT,Blocked-by:this card; #14238 sequences after it.
Session:session_0174WZTU6XcFcS7g2kykC53i(GitHubzhuangjianguo)
Branch:claude/issue-14168-field-value-domain
Claimed at 2026-09-04T00:43Z; queue read 00:40Z (oldest ruled card in the lane'spm:queue; total order p0 >pm:blocking>target:> Bug > rest by age).
File surface (spec only):packages/spec/src/data/field.zod.ts(the slot + applicability),packages/spec/src/system/settings-manifest.zod.ts(the vocabulary moves to a shared module and is referenced from here — no member added, none removed), one new shared module for the vocabulary + membership predicate underpackages/spec/src/(path chosen by measurement), their tests, the field error-code declaration the write path will emit (beside themaxLength/minfamily's), generated artifacts (api-surface, export-origins, declaration-map, authorable-surface, json-schema manifest, reference docs), the hand-written docs page(s) that list field constraints, a@objectstack/specchangeset. ⛔packages/services/**(service-settings'value-domains.tsre-points in its own card),packages/objectql/**/packages/core/**(enforcement card), objectui,content/docs/releases/**.
Clause ② yes (a published field schema widens its accept set) —needs:contract-reviewhung on this card in this stroke; the dev hangs it on the PR. Contract review at tier by this seat. H17:field.zod.tsis a declaredRestart-touchtrigger file of the v18 hold #8345 — notification posted there, no rider taken.
Generated by Claude Code
Dispatch (R5 of this shift, 2026-09-04T01:39Z) —
domain:specseat,session_0174WZTU6XcFcS7g2kykC53i, seat post #6017.mode:subagent,model: fable(CONTRACT_REVIEW_TIER), size M, Clause ② yes (dual carrier:needs:contract-reviewhung on this card at claim; the dev hangs it on the PR). The dev leaves its ownClaim:comment below carryingClause-②: yesand its file surface before its first edit. Launch was deferred from the 00:43Z claim to 2026-09-04T01:39Z by the tier's five-hour rate limit (reset 01:20Z) — recorded so the gap between claim and dispatch reads as what it was.Scope handed to the dev — the SPEC half of ruling A (comment 5507503059, maintainer verbatim 「同意」 2026-09-02), contract-first: (1) the three-member vocabulary moves to one shared module,
SpecifierValueDomainSchemakeeps name and shape by reference — no member added or removed; (2)FieldSchema.valueDomain?with a.describe()and an applicability rule on the string-storing family (measured againstmaxLength's list; the choice stated in the PR body), refused elsewhere at parse with a located issue via themaxLength/minLengthapplicability mechanism; (3) one exported membership predicate in spec —iana_time_zone= theIntl.DateTimeFormatprobe (pins:UTC,Asia/Kolkata,Europe/Kyivaccepted;Europe/Munichrefused), ISO 4217 from the spec's own snapshot if complete, ISO 3166 alpha-2 exact uppercase; the services re-point is declared symbol-by-symbol in the PR body, ⛔packages/services/**untouched; (4) the refusal contract declared beside themaxLength/minfamily's only if that family declares codes in spec — otherwise reported, not invented; (5) generated products, hand-written constraint docs, liveness / strictness ledgers as the gates demand, census--fix; (6) changeset@objectstack/specminor, additive. Read-and-report for the follow-up cards this seat files at ACCEPT: engine write-path enforcement site (domain:engine,Blocked-by: #14168; #14238 sequences after), the services re-point (domain:services), and whether objectui's form layer reads field keys generically. H17: notification on #8345 (5534029200), no rider.
Generated by Claude Code
4 remaining items
os-dev-report
{ "issue": 14168, "status": "done", "branch": "claude/issue-14168-field-value-domain", "pr": "https://github.com/objectstack-ai/objectstack/pull/15133", "premise_still_valid": true, "summary": "Spec half of ruling A delivered at head 18c6c7f49 (draft PR #15133, needs:contract-review hung and read back). FieldSchema gains valueDomain drawn from the shared three-member ValueDomainSchema (new packages/spec/src/shared/value-domain.zod.ts; SpecifierValueDomainSchema is now an identity alias of it, same name and members), authorable on text only (measured narrower than the twelve-type maxLength family; refused elsewhere at parse through the same superRefine door with a located issue naming the type), plus the shared predicate isValueDomainMember (iana_time_zone = Intl.DateTimeFormat probe; iso_4217_currency = the spec's CLDR snapshot keys, measured equal both ways to Intl.supportedValuesOf('currency') at 162 on node v22.22.2; iso_3166_alpha2 = the explicit 249-code list, exact uppercase), the FieldErrorCode member value_domain (ADR-0114 D1/D2) with four-locale message templates, a planned liveness row, regenerated artifacts (incl. eight skills/*/references/_index.md pointer lines, a certified pure regeneration on the governed surface) and six hand-written docs pages. Premise re-verified on origin/main: the enum lived only in settings-manifest.zod.ts, FieldSchema had no valueDomain, and the predicate lived only in service-settings. Not touched: packages/services, packages/objectql, packages/core, objectui, content/docs/releases, and the two spec authoring forms (field.form.ts / object.form.ts) which are hand-written visibleWhen lists and would advertise an inert key before the engine card enforces it. PM claim overlap: the assignee was already the PM's; origin/main was merged before opening (one overlapping census-owned file, regenerated on the merged tree; the deferred regeneration discharged in its own commit). Read-and-report for the follow-ups is in the PR body: (a) record-validator.ts:568-583 string branch, fail() at :486, FieldDef at :156-172; (b) value-domains.ts symbol-by-symbol re-point (isIanaTimeZone, iso4217Codes, ISO_3166_ALPHA2 / ISO_3166_ALPHA2_CODES, DOMAIN_MEMBERSHIP) onto @objectstack/spec/shared; (c) objectui reads FieldSchema keys from a hand-written inspector (ObjectFieldInspector.tsx:626-641), SchemaForm is generic but excludes Object/Field designers, so a Studio control is an objectui edit.", "tests": "All at final head 18c6c7f49, exits captured before any pipe. spec build under the lock: 'check-dts-emitted: @objectstack/spec - 34/34 declared declaration file(s) present.' / 'VERDICT command-exit 0'. vitest (value-domain, field, settings-manifest, errors, validation-message, type-alias pin, currency-precision-iso4217) under the lock: 'Test Files 7 passed (7)' / 'Tests 416 passed (416)' / 'VERDICT command-exit 0'. spec typecheck exit 0: 'check:test-typecheck: OK — 54 file(s) / 261 error(s) / 145 pinned signature(s)' (ledger unchanged; tsc --listFiles -p tsconfig.test.json lists all six edited/new test files). check:generated clean second run exit 0: 'All 15 generated artifacts are up to date.' (liveness 'field 93 classified (live 89, planned 3, dead 1)'). eslint --no-inline-config over the 12 edited TS files exit 0. check:doc-authoring: '14725 customer-facing string(s) ... clean — no internal issue-id references'. check:nul-bytes: 'check-nul-bytes: OK (scanned 8229 text file(s) ...)'. check-adr-0087-registration --base 2cc4610: 'this PR adds no declared-breaking changeset (1 non-breaking changeset(s) seen)' (the gate judges only declared-breaking changesets, so an additive key needs no marker; the house-style not-required (accept-set expansion) comment is present anyway); check-changeset-no-major: 'This diff introduces no major bump.' Downstream @objectstack/lint (closure built, lock): typecheck OK, 'Test Files 94 passed (94)' / 'Tests 2900 passed, 5 skipped'. Also exit 0: check:error-code-provenance, exported-any, dual-source-exports, browser-reachable-entries, entry-nameability, empty-state, strictness-ledger, variant-docs, yaml-examples, llms-txt (206->207, shared 12->13 corrected with prose), quick-reference-counts (Shared Protocol total 7->8), spec-parsed-alias, error-code-casing, system-context-census (--fix re-anchored field.zod.ts:1540->1580 after the last edit), type-check-coverage, lint check:doc-formula-expressions / check:doc-security-posture (after building lint itself). dispatch-gates.mjs --repo objectstack-ai/objectstack --commands derived 89 commands at 18c6c7f49 (identical list to the pre-merge derivation): 85 exit 0; NOT MEASURED in the gates' own words: check:dual-build-cjs-loads ('PREREQUISITE NOT MET — this gate reads built output, and some package has no dist/ ... nothing was measured'), check:type-check-debt ('PREREQUISITE NOT MET ... --re-measure cannot run: 30 workspace dependenc(ies) ... have no built type entry point'), check:skill-examples ('packages/client-react/dist holds no .d.ts declarations — the package is not built'), check:react-declaration-parity ('needs MANIFEST=sdui.manifest.json') — all need the full monorepo build or objectui's manifest, CI's run. Reverse verification from committed state ab05a825a: removed the valueDomain slot line (grep -c 1->0; git diff --stat 1 deletion), 'vitest src/data/field.test.ts -t valueDomain': 'Tests 23 failed, 2 passed, 211 skipped' (direction red as expected; the two survivors are positive controls that do not read the slot); restore via git checkout HEAD -- path proven: git hash-object == HEAD: blob 5b6f87db72c19cb856e1e980a8f05c4e3ac6e7f0, git diff HEAD --stat empty, porcelain empty. No build in the ablation's resolution path: field.test.ts imports ./field.zod as relative source, not dist.", "mcp_calls": "2 GitHub MCP calls (search_issues x2: control word + targeted duplicate check, after the REST search class answered 403 'sessions are bound to their configured repositories'; channel switch declared). Everything else REST/git. Plus 1 Claude Code Remote subscribe_pr_activity call (PR #15133 subscribed).", "open_questions": [ { "question": "Where do the two spec authoring-form rows (field.form.ts / object.form.ts visibleWhen for valueDomain on text) land: this PR, or the engine card?", "options": ["A: engine card adds them in the same stroke it flips the liveness row planned->live (declared = enforced = shown)", "B: add them to this PR now (the key would show in Studio's metadata forms before any write path enforces it)"], "recommendation": "A, because the forms are hand-written lists that today show maxLength/minLength for exactly the enforced set, and showing an unenforced key is the ADR-0078 / Prime Directive #10 shape; the PM can ask for B in contract review and it is a two-line change." }, { "question": "Does the settings door's domain refusal move from invalid_value to the new value_domain catalog member when services re-points onto the shared predicate?", "options": ["A: yes, one code for one constraint kind on both doors (a wire-visible change on the settings door, ADR-0114 D2)", "B: no, settings keeps invalid_value with constraint { valueDomain } and only fields emit value_domain"], "recommendation": "A, because the ruling is one vocabulary and one predicate for both doors and the catalog's rule is that the code is the constraint's own name; the services card should carry it with a pin, not this PR." } ], "out_of_scope_findings": [ "filed as #15134: packages/core/src/security/resolve-authz-context.ts:936 carries a third module-private copy of the IANA time-zone probe that @objectstack/spec/shared now exports as the shared predicate (finding label, unassigned, Blocked-by #14168)" ] }Generated by Claude Code
Generated by Claude Code
Seat decision on PR #15133's CI red (surface, not contract) — widen by one test file, same PR.
domain:specPM seat, sessionsession_0174WZTU6XcFcS7g2kykC53i(seat post #6017), 2026-09-04T02:50Z.CI on
18c6c7f4:Test Core (5/6)andTemporal Conformance (live PG + MySQL)are red with ONE failing test between them —packages/drivers/driver-sql/src/builtin-column-collision.test.ts› "the FieldSchema storage/presentation classification (#12015) › classifies EVERY FieldSchema key, and invents none":unclassified FieldSchema key(s): expected [ 'valueDomain' ] to deeply equal [](jobs 100892278254 / 100892278038, read from their logs at 02:49Z). That ratchet requires everyFieldSchemakey to be classified as storage or presentation in its own table; the newvalueDomainkey is not, by construction — no spec-only shape greens it (the same structure as #14149's ledger ratchet, ruled 5533840057 today).Ruling (open to the maintainer's veto): widen this card's file surface by exactly that one test file, on the four-condition adjacent-fix path — same defect class (a
FieldSchema-key ratchet learning a key the ruling added), mechanical (the assertion names the key and the table), the file is claimed by no one (26 open PRs at 2026-09-04T02:50Z, none touches it), same gate family (Test Core / Temporal). Concretely: classifyvalueDomainin that table besidemaxLength/minLength(it is a write-path constraint on the stored string, never a column — the same class as the bounded-string family; read the table's own docblock and place it where the family sits); no other change inpackages/drivers/**; the ratchet keeps refusing an unclassified key. Bookkeeping: the dev appends the widened file to its own claim comment in place, the PR body names the widening with this comment and the before/after readings of that test file,git rev-parse --short HEADafter the last commit copied into the body and the report. Clause ② stays yes;needs:contract-reviewstays hung on both carriers until the review.The governed-surface reading (the eight
skills/*/references/_index.mdpointer lines) is a separate, read-only ask already sent to the dev: the mechanicalcheck-governed-merges.mjs --testverdict at the head from a tree where the generator runs — the seat's own runs failed on an environment error (tsxnot found underpnpm exec), which is not a verdict.Correction (2026-09-04T02:51Z, same seat): the classification table the ratchet reads is not in the test file — the test imports
FIELD_KEY_STORAGE_CLASSfrompackages/drivers/driver-sql/src/builtin-column-collision.tsand says "Classify it inFIELD_KEY_STORAGE_CLASS". The one widened file is therefore that source module (one table entry forvalueDomain, placed with the bounded-string familymaxLength/minLength), not the test; the test file itself is left untouched. Claim reading corrected too: PR #11336 (open) touchesbuiltin-column-collision.test.ts, which is one more reason to leave the test alone; the source module's claim status is in the dev's round-2 report (0 open PRs touch it at 2026-09-04T02:51Z by this seat's scan if the dev's grep agrees). Everything else in the ruling stands.
Generated by Claude Code
os-dev-report
{ "issue": 14168, "status": "done", "branch": "claude/issue-14168-field-value-domain", "pr": "https://github.com/objectstack-ai/objectstack/pull/15133", "premise_still_valid": true, "summary": "Read-only follow-up round for the contract review's governed-surface question, taken at head 18c6c7f49 in a fresh detached worktree at origin/claude/issue-14168-field-value-domain with pnpm install done (exit 0; the tree stayed clean before and after both readings — git status --porcelain 0 lines). No code changes, nothing pushed. Reading 1, node scripts/pm/check-governed-merges.mjs --test over the eight skills/*/references/_index.md paths: exit 0 (captured by redirect), verdict 'governed-surface predicate: 0 of 8 path(s) hit the register (5 surfaces, repo-agnostic).' / '✅ NOT governed — ordinary queue landing applies to a PR with exactly this file list.' and, for each of the eight paths verbatim, 'ℹ️ generated-surface exception (#11705): skills/<name>/references/_index.md is a PURE REGENERATION — byte-equal to `pnpm --filter @objectstack/spec gen:skill-refs` recomputed on THIS tree (never a stored baseline), so this path does not govern the PR by itself. Any other governed hit still forks the whole PR.' — lifted for all eight (objectstack-ai, objectstack-api, objectstack-automation, objectstack-data, objectstack-i18n, objectstack-platform, objectstack-query, objectstack-ui). Reading 2, pnpm --filter @objectstack/spec exec tsx scripts/build-skill-references.ts --check --generated-manifest=/tmp/skill-refs-manifest.json: exit 0, last line '✅ 9 generated files in sync with packages/spec'; the manifest is { surface: 'skills/*/references/_index.md', mode: 'check', outputs: [nine paths] } — the eight above plus skills/objectstack-formula/references/_index.md. The ninth is the generator's whole output set, not a drift: it is byte-identical to the merge-base (git diff --stat 2cc4610304c58c3de05a36efbb7cd1710b9d440e HEAD on that path is empty; the formula skill enumerates 2 core + 0 deps, so the new shared module adds no line there), and my diff touches exactly 8 index files. No drift reported by either reading, so per the coordinator's instruction I stopped after the readings.", "tests": "Reading 1: node scripts/pm/check-governed-merges.mjs --test <eight paths> > log 2>&1; exit 0; verdict lines quoted in summary (28 output lines, all eight paths lifted under the generated-surface exception #11705, predicate '0 of 8 path(s) hit the register'). Reading 2: pnpm --filter @objectstack/spec exec tsx scripts/build-skill-references.ts --check --generated-manifest=/tmp/skill-refs-manifest.json > log 2>&1; exit 0; '✅ 9 generated files in sync with packages/spec'; manifest outputs = the eight PR paths + skills/objectstack-formula/references/_index.md (unchanged vs merge-base 2cc4610304c58c3de05a36efbb7cd1710b9d440e, confirmed by an empty git diff --stat). Both at head 18c6c7f49; the earlier 254 the seat saw was tsx missing from its environment, which this worktree does not reproduce (tsx resolves after pnpm install).", "mcp_calls": "0 in this round (REST + git only); cumulative for the card: 2 GitHub MCP search_issues calls and 1 Claude Code Remote subscribe_pr_activity call, as reported in comment 5534923686.", "open_questions": [], "out_of_scope_findings": [] }Generated by Claude Code
Generated by Claude Code
- added a commit that references this issue
on Sep 4, 2026 os-dev-report
{ "issue": 14168, "status": "done", "branch": "claude/issue-14168-field-value-domain", "pr": "https://github.com/objectstack-ai/objectstack/pull/15133", "premise_still_valid": true, "summary": "Patch round done at final head 750d24603 (pushed fast-forward on top of 18c6c7f49; no rebase, amend or force). Per the domain:spec seat's ruling (comment 5534967364 with its in-place correction) the card's surface widened by exactly one SOURCE module, packages/drivers/driver-sql/src/builtin-column-collision.ts: one FIELD_KEY_STORAGE_CLASS entry, valueDomain: 'presentation', placed beside minLength with the reason in its trailing comment — the table's docblock draws the line at what the DDL reads (maxLength is 'storage' because createColumn sizes the column; minLength is 'presentation' as write-time validation), and valueDomain is write-time membership validation of the WRITTEN string that createColumn never reads, the column staying the string maxLength sizes; the docblock makes no case for 'storage'. The test file builtin-column-collision.test.ts is untouched (PR #11336 holds it) and nothing else under packages/drivers/** moved; no sibling landing on origin/main touches the module since the branch base (git log f594e70d..origin/main on that path is empty, which agrees with the seat's 0-open-PR scan). Bookkeeping done: the widened file appended in place to my own claim comment 5534542455 (Clause-②: yes kept, read back), the PR body patched with the widening, the ruling citation, the before/after readings and the final-head line (read back; the PATCH edit downgraded the footer to the bare form, the platform's known behaviour). needs:contract-review stays hung on both carriers. Commit trailers are the harness-written pair (Co-Authored-By + Claude-Session), exempt from the content-artifact model-identifier ban by the maintainer's 2026-09-03 ruling recorded in the dispatch runbook; no model identifier in the PR title/body, code, comments, docs or changeset. Patch worktree removed without force afterwards.", "tests": "All at 750d24603 in a fresh worktree on the branch (driver-sql dependency closure built under the lock), exits captured by redirect. BEFORE the entry (at 18c6c7f49): 'pnpm --filter @objectstack/driver-sql exec vitest run --maxWorkers=2 src/builtin-column-collision.test.ts' exit 1 — 'Tests 1 failed | 10 passed (11)' — 'AssertionError: unclassified FieldSchema key(s): expected [ 'valueDomain' ] to deeply equal []'. AFTER the entry: same command exit 0 — 'Tests 11 passed (11)' — 'VERDICT command-exit 0'; verbose reporter: '✓ … classifies EVERY FieldSchema key, and invents none', '✓ … puts the honoured half on the presentation side and the column shape on the storage side', '✓ … puts required on the PRESENTATION side'. 'pnpm --filter @objectstack/driver-sql typecheck' (tsc --noEmit) exit 0, 0 errors. eslint --no-inline-config over the module exit 0, no output. 'pnpm --filter @objectstack/spec check:generated' exit 0 — 'All 15 generated artifacts are up to date.' Spec pin set (value-domain, field, settings-manifest, errors, validation-message, type-alias pin, currency-precision-iso4217) under the lock: 'Test Files 7 passed (7)' / 'Tests 416 passed (416)' / 'VERDICT command-exit 0'. 'node scripts/check-system-context-census.mjs' exit 0 — 'check-system-context-census: OK — 106 elevation read sites in 20 packages across 45 files, all anchored; 140 anchors resolve, 27 declared non-read.' The earlier read-only round (comment 5534967664) stands: check-governed-merges.mjs --test exit 0, all eight skills/*/references/_index.md lifted under the generated-surface exception #11705; the generator's own --check exit 0 with a nine-path manifest (the eight plus the unchanged objectstack-formula index).", "mcp_calls": "0 in this round (REST + git only); cumulative for the card: 2 GitHub MCP search_issues calls and 1 Claude Code Remote subscribe_pr_activity call.", "open_questions": [], "out_of_scope_findings": [] }Generated by Claude Code
Generated by Claude Code
- added a commit that references this issue
on Sep 4, 2026 os-dev-report
{ "issue": 14168, "status": "done", "branch": "claude/issue-14168-field-value-domain", "pr": "https://github.com/objectstack-ai/objectstack/pull/15133", "premise_still_valid": true, "summary": "Merge round done; final head b6f10508a, pushed fast-forward (no rebase, amend or force; origin/main fcc42e6c1 is now an ancestor of HEAD, and GitHub reads PR #15133 as mergeable: true, mergeable_state: blocked — no longer dirty — with needs:contract-review still hung). In a fresh dedicated worktree on the branch: origin/main was merged as merge commit ef6772673 via scripts/pm/os-regen-merge.sh (steps 1-3; its step-3 commit was refused by the os-regen pre-commit hook exactly as the helper's header says is the designed collection point, naming two deferred artifacts), then the two artifacts the driver deferred were regenerated on the merged tree by the repo's own generators — content/docs/references/index.mdx via check:generated --fix (gen:schema + gen:docs; schema totals 1590 to 1593 and automation 69 to 72, main's additions) and content/docs/permissions/system-context.mdx via gen:system-context-census (one anchor, protocol.ts:1747 to :1795, main's move) — and committed as regeneration commit b6f10508a after inspecting the STAGED diff (exactly those two files; the hook printed 'os-regen: all deferred artifacts are current — marker cleared'). The strictness counts ledger and the skills indexes were not deferred: main moved the counts ledger and the branch did not (step 2 kept main's side; check:strictness-ledger confirms it current), and the eight skills indexes were changed only by the branch (kept). No behaviour changed. PR body updated: final-head line refreshed to b6f10508a plus one sentence naming the merge commit and the regenerated artifacts (read back). Trailers on the regeneration commit are the model-free pair the seat asked for (Claude-Session: + Co-authored-by: Claude <noreply@anthropic.com>); the merge commit carries git's default message. For the seat's reading, the dispatch runbook's exemption sentence (origin/main, dispatch-runbook.md:246-248) is, verbatim: 「模型标识只禁内容工件(维护者 2026-09-03 裁):PR 标题/正文、代码注释、文档、changeset、issue/PR 评论;两条豁免同属规则 —— harness 写在 commit 的 `Co-Authored-By` 尾注(含 session 链接),与 `.claude/agents/*` frontmatter 的 `model:`(`check:agent-model-declared` 要求);⛔ 不再申报为偏差。」 — the branch's three earlier commits (9dc7ac567, ab05a825a, 18c6c7f49, 750d24603) carry the harness-written trailer that sentence exempts; this round's commit is written model-free regardless, as instructed. Merge worktree removed without force afterwards (exit 0; no issue-14168 worktree remains).", "tests": "All at b6f10508a in the merge worktree, exits captured by redirect. pnpm --filter @objectstack/spec build (lock): 'check-dts-emitted: @objectstack/spec - 34/34 declared declaration file(s) present.' / 'VERDICT command-exit 0'. check:generated second clean run exit 0: 'All 15 generated artifacts are up to date.' pnpm --filter @objectstack/spec typecheck exit 0: 'check:test-typecheck: OK — 54 file(s) / 261 error(s) / 145 pinned signature(s)'. Spec pin set (value-domain, field, settings-manifest, errors, validation-message, type-alias pin, currency-precision-iso4217) under the lock: 'Test Files 7 passed (7)' / 'Tests 416 passed (416)' / 'VERDICT command-exit 0'. Driver-sql closure built under the lock, then 'pnpm --filter @objectstack/driver-sql exec vitest run --maxWorkers=2 src/builtin-column-collision.test.ts': 'Tests 11 passed (11)' / 'VERDICT command-exit 0'. check:strictness-ledger exit 0: '✓ docs/audits/2026-07-unknown-key-strictness-ledger.counts.md is current — 440 site(s) measured, 1 authorable strip site(s) left.' node scripts/check-system-context-census.mjs exit 0 (after gen:system-context-census rewrote 1 anchor on the merged tree): 'check-system-context-census: OK — 106 elevation read sites in 20 packages across 45 files, all anchored; 140 anchors resolve, 27 declared non-read.' node scripts/pm/check-governed-merges.mjs --test over the eight skills/*/references/_index.md paths exit 0: 'governed-surface predicate: 0 of 8 path(s) hit the register (5 surfaces, repo-agnostic).' / '✅ NOT governed — ordinary queue landing applies to a PR with exactly this file list.' and for each of the eight, verbatim, 'ℹ️ generated-surface exception (#11705): skills/<name>/references/_index.md is a PURE REGENERATION — byte-equal to `pnpm --filter @objectstack/spec gen:skill-refs` recomputed on THIS tree (never a stored baseline), so this path does not govern the PR by itself. Any other governed hit still forks the whole PR.' (8 lifted lines counted). Working tree clean after every step (git status --porcelain 0 lines).", "mcp_calls": "0 in this round (REST + git only); cumulative for the card: 2 GitHub MCP search_issues calls and 1 Claude Code Remote subscribe_pr_activity call.", "open_questions": [], "out_of_scope_findings": [] }Generated by Claude Code
Generated by Claude Code
Director audit of the contract — PASS (summon 13, session_01WXyGTWPbbreqXow7Z2pZCk; tier fuse read 2026-09-04T01:26Z,
get_sessionservedclaude-fable-5-1=CONTRACT_REVIEW_TIER). Subject: PR #15133 at headb6f10508a(the merged head after the seat's adjacent-fix and merge rounds). The spec seat's in-seat review owns the release; this reading is recorded so the seat can land on it without a second pass, and the stall clause applies at this seat's next check-in if the head is green and unlanded.Implemented-by: claude/issue-14168-field-value-domain (mode:subagent dev under session_0174WZTU6XcFcS7g2kykC53i)
Reviewed-by: session_01WXyGTWPbbreqXow7Z2pZCk① Derived judgments
FieldSchema.valueDomain: one new optional key drawn from the closed three-memberValueDomainSchema, authorable ontextonly through the same superRefine doormaxLength/minLengthuse, refused with a located issue naming the type elsewhere. Correct, and narrower than the bounded-string family for the reasons the docblock measures;selectrightly excluded.SpecifierValueDomainSchemabecomes an identity alias of the shared schema (pinnedtoBe), same name, same members: importers do not move. Correct.isValueDomainMemberandISO_3166_ALPHA2_CODESlive inpackages/spec/shared. The old settings-manifest docblock said the code list must not live in spec; the ruling asks for one predicate shared by two doors, and spec already ships pure verdicts over checked-in data (the CLDR currency snapshot). A pure, stateless predicate over a checked-in list is declaration-adjacent, not execution; accepted on that precedent, stated here so the judgment is visible.FieldErrorCodegainsvalue_domain(ADR-0114 D1 naming, D2 new-kind rule) with templates in all four locales; the driver-sql storage-class entry ispresentation, matchingminLength. Correct.- Enforcement is deliberately not in this PR: the liveness row is
planned, the Studio form rows are withheld, and the changeset says so. That is the honest shape for a sequenced pair; it must not stay open-ended.
② Semver:
@objectstack/specminor, additive, no ADR-0087 marker owed. Accepted.③ Boundary flags
- The engine half (write-path refusal in
record-validator.ts, ledger row tolive, the two form rows) and the services half (re-pointvalue-domains.tsonto the shared predicate) are the two follow-up cards the seat files at ACCEPT; each must carryBlocked-by:this PR and be named on this card, or the declared key is ADR-0049's inert shape. - Open question 2 (whether the settings door's refusal code moves from
invalid_valuetovalue_domain) belongs to the services card.
Landing gate as of 03:15Z:
mergeable_state: blockedwith CI in progress onb6f10508a; the eightskills/*/references/_index.mdlines are certified pure regeneration (check-governed-merges --test: 0 of 8 governed), so the queue is the landing path once green.
Generated by Claude Code
Contract review (Clause ②) on PR #15133 — PASS · ACCEPT: comment 5535158979 (2026-09-04T03:17Z), by the
domain:specPM seat, sessionsession_0174WZTU6XcFcS7g2kykC53i, seat post #6017; tier fuse read 03:14Z,--pair 15133exit 0; governed reading in (eight skills pointer lines lifted as pure regeneration). Seat rulings recorded there (open to veto):text-only applicability accepted; the driver-sql one-file widening (5534967364); open question 1 → A (form rows land with the engine card's liveness flip); open question 2 → A (one codevalue_domainon both doors, carried by the services card).needs:contract-reviewcleared on both carriers in this stroke. Landing at all-green onb6f10508(18 checks running at 03:15Z); on MERGED this card closes viaFixes,pm:dispatchedis stripped, landing note here, the engine / services / objectui follow-up cards are filed.
Generated by Claude Code
Landed — PR #15133 merged via the queue at 2026-09-04T04:05:20Z, merge commit
1d7e76a6(squash;origin/maintip5c584231aat 04:06Z, the merge commit is its ancestor).domain:specPM seat, sessionsession_0174WZTU6XcFcS7g2kykC53i, seat post #6017. Contract review PASS · ACCEPT 5535158979 (director audit 5535155127 concurs); provenance 5535273829.Probed on
origin/mainat 04:06Z:shared/value-domain.zod.ts—ValueDomainSchema(:97),ISO_3166_ALPHA2_CODES(:123),isValueDomainMember(:175), re-exported fromshared/index.ts:35;data/field.zod.ts— thevalueDomainslot (:1022) and the applicability refinement (:1941-1944);system/settings-manifest.zod.ts:156—SpecifierValueDomainSchema = ValueDomainSchema;api/errors.zod.ts:268—value_domain; the liveness rowplanned(packages/spec/liveness/field.json:224); the driver-sql storage-class table carriesvalueDomain: 'presentation'(builtin-column-collision.ts:100, the one-file widening ruled in comment 5534967364).Card closed by the PR's
Fixes;pm:dispatchedstripped in this stroke (read back). Thefield.zod.ts/settings-manifest.zod.ts/shared/value-domain.zod.ts/errors.zod.ts/validation-message.tsreservation and the one-filebuiltin-column-collision.tswidening are released. Follow-up cards filed at 2026-09-04T04:10Z: #15161 (domain:enginewrite-path enforcement inrecord-validator.ts, the two authoring-form rows pre-authorised for that card, the liveness flip; #14238 sequences after it —pm:queue,domain:*triage's), #15162 (domain:servicesre-point ofvalue-domains.tsonto@objectstack/spec/sharedandvalue_domainon the settings door —pm:queue,domain:*triage's) and objectui#7597 (the Studio control;pm:on-holdwith an installabilityRestart-when:— objectui consumes the spec as a published package). H17 landing notice on #8345: comment 5535529560. Seat rulings recorded in the review (open to veto):text-only applicability; the form rows land with the engine card; one codevalue_domainon both doors.
Generated by Claude Code
- added a commit that references this issue
on Sep 9, 2026 - added a commit that references this issue
on Oct 7, 2026
Found while adding "this must be a real IANA zone" to an application field (
duly_duty.timezone, objectstack-ai/duly#24). Filing per that app's rule "if the platform genuinely cannot express it, file upstream rather than quietly working around it".The gap
packages/specalready publishes the vocabulary and, valuably, the definition of membership —SpecifierValueDomainSchemainsystem/settings-manifest.zod.ts:with the note that for
iana_time_zone"membership is theIntl.DateTimeFormatprobe … NOTIntl.supportedValuesOf('timeZone')". That note is correct and it saved me a bug — see the measurement below.But
valueDomainis reachable only from a settingsSpecifier. An object field has no equivalent.FieldTypehas notimezonemember, andFieldSchemacarriesmaxLength/minLength/min/maxbut no value-domain slot. So an authored zone field is a bareField.textand nothing checks it; the value is discovered to be wrong wherever it is finally consumed.Why an application cannot close it at its own layer either
Both extension points an app can reach were measured on
@objectstack/spec/@objectstack/runtime/@objectstack/objectql17.2.0, Node v22.22.2.1. A
script/cross_fieldvalidation rule (CEL) cannot. The whole stdlib registered in@objectstack/formula'sregisterStdLibis:There is no zone/currency/country oracle, and no app-level way to register one —
buildEnvis internal to the package. The only reachable spelling ismatches(record.timezone, '<regex>'), which either checks shape only (Europe/Munichpasses) or freezes a tzdata snapshot into metadata — precisely what theiana_time_zonenote warns is a measurably different set from what the host accepts.2. An L2 hook
bodycannot: the sandbox has noIntl. Measured directly against the runtime's own sandbox (quickjs-emscripten0.32.0,newQuickJSWASMModule, the variantAppPluginwires throughQuickJSScriptRunner):HookBodyCapabilityisapi.read | api.write | api.transaction | crypto.uuid | log— nothing grantsIntl, so this is not a capability the author forgot to declare.The part that makes this actively hazardous, not merely missing
objectstack buildsilently lowers a self-contained inline handler into an L2body— confirmed in a real artifact, where a hook authored ashandler: <inline fn>ships as:{ "handler": "duly_task_lifecycle_stamps", "body": { "language": "js", "source": "…", "capabilities": [] } }and
resolveHandlerinbindHooksToEngineprefersbodyand ignoreshandlerwhenever both are present. So the natural way to write this check — an inlinebeforeInserthandler doing theIntl.DateTimeFormatprobe — behaves like this:pnpm validate,typecheck,testandbuildare all green (in-process tests run the raw JS function in Node, whereIntlexists);ReferenceError: Intl is not definedinside the sandbox;onError: 'abort'that a validation-shaped hook must declare, every write to that object is refused.Nothing anywhere in that sequence says the handler moved to a runtime that lacks the global it uses.
The only route that keeps the probe in Node is the string handler ref (
handler: 'my_fn'+defineStack({ functions })), resolved against the bundle functions map +runtimeModule. That works — buthook.zod.tsmarkshandler"DEPRECATED, preferbody" andwarnLegacyHandlerprints "Move the handler source intoHook.body". Following that advice on any host-API-dependent check converts a working guard into a refuse-everything hook. The deprecation direction and the only viable route for this class of check point opposite ways.What would close it
In rough order of preference:
valueDomainon a field —Field.text({ valueDomain: 'iana_time_zone' }), enforced on the write path with the membership definition already written down insettings-manifest.zod.ts. Declarative, translatable refusal message, visible to Studio / OpenAPI / the form layer, and one oracle for settings and fields alike.formatvalidation member alongsideemail | url | phone | jsondrawn from the same closed vocabulary.hook.zod.tsthat a body cannot reach host intrinsics and that the string handler ref is the supported route for checks that need them, so the deprecation does not read as advice to break them.Independently of (1)-(3), the sandbox-lowering hazard seems worth a build-time diagnostic on its own: an inline handler that references a global the sandbox does not provide is lowerable, buildable, testable and broken only in production.
Meanwhile
objectstack-ai/duly is using the string handler ref with the
Intl.DateTimeFormatprobe in Node, sharing one oracle with the period engine that consumes the value, and pointing at this issue in the code.Measurement backing the
iana_time_zonenote, since it is the crux and it is worth having twice — Node v22.22.2:A field constrained against the enumerated list would reject
UTC— the platform's own declared default.Generated by Claude Code