Skip to content

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

@os-warren

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/spec already publishes the vocabulary and, valuably, the definition of membership — SpecifierValueDomainSchema in system/settings-manifest.zod.ts:

'iana_time_zone' | 'iso_4217_currency' | 'iso_3166_alpha2'

with the note that for iana_time_zone "membership is the Intl.DateTimeFormat probe … NOT Intl.supportedValuesOf('timeZone')". That note is correct and it saved me a bug — see the measurement below.

But valueDomain is reachable only from a settings Specifier. An object field has no equivalent. FieldType has no timezone member, and FieldSchema carries maxLength / minLength / min / max but no value-domain slot. So an authored zone field is a bare Field.text and 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/objectql 17.2.0, Node v22.22.2.

1. A script / cross_field validation rule (CEL) cannot. The whole stdlib registered in @objectstack/formula's registerStdLib is:

now today daysFromNow daysAgo isBlank coalesce trim joinNonEmpty daysBetween
addDays addMonths date datetime abs round floor ceil min max upper lower
contains startsWith endsWith matches len isEmpty

There is no zone/currency/country oracle, and no app-level way to register one — buildEnv is internal to the package. The only reachable spelling is matches(record.timezone, '<regex>'), which either checks shape only (Europe/Munich passes) or freezes a tzdata snapshot into metadata — precisely what the iana_time_zone note warns is a measurably different set from what the host accepts.

2. An L2 hook body cannot: the sandbox has no Intl. Measured directly against the runtime's own sandbox (quickjs-emscripten 0.32.0, newQuickJSWASMModule, the variant AppPlugin wires through QuickJSScriptRunner):

typeof Intl  -> undefined
typeof Date  -> function
typeof JSON  -> object

HookBodyCapability is api.read | api.write | api.transaction | crypto.uuid | log — nothing grants Intl, so this is not a capability the author forgot to declare.

The part that makes this actively hazardous, not merely missing

objectstack build silently lowers a self-contained inline handler into an L2 body — confirmed in a real artifact, where a hook authored as handler: <inline fn> ships as:

{ "handler": "duly_task_lifecycle_stamps",
  "body": { "language": "js", "source": "…", "capabilities": [] } }

and resolveHandler in bindHooksToEngine prefers body and ignores handler whenever both are present. So the natural way to write this check — an inline beforeInsert handler doing the Intl.DateTimeFormat probe — behaves like this:

  • pnpm validate, typecheck, test and build are all green (in-process tests run the raw JS function in Node, where Intl exists);
  • in production the lowered body throws ReferenceError: Intl is not defined inside the sandbox;
  • with the 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 — but hook.zod.ts marks handler "DEPRECATED, prefer body" and warnLegacyHandler prints "Move the handler source into Hook.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:

  1. valueDomain on a field — Field.text({ valueDomain: 'iana_time_zone' }), enforced on the write path with the membership definition already written down in settings-manifest.zod.ts. Declarative, translatable refusal message, visible to Studio / OpenAPI / the form layer, and one oracle for settings and fields alike.
  2. A format validation member alongside email | url | phone | json drawn from the same closed vocabulary.
  3. Failing both: state in hook.zod.ts that 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.DateTimeFormat probe 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_zone note, since it is the crux and it is worth having twice — Node v22.22.2:

Intl.supportedValuesOf('timeZone').length          -> 418
  includes 'UTC'                                   -> false
  includes 'GMT'                                   -> false
  includes 'Asia/Kolkata'                          -> false
  includes 'Europe/Kyiv'                           -> false
  includes 'US/Eastern'                            -> false
new Intl.DateTimeFormat('en-US', { timeZone: v })  -> resolves for every one of them

A field constrained against the enumerated list would reject UTC — the platform's own declared default.


Generated by Claude Code

Activity

  1. huangyiirene commented on Sep 2, 2026

    @huangyiirene
    Collaborator

    Triage (R+89, triage seat, session session_019kDRpB7D2XzVzkaLp57T5D): graded → needs-user-decision · priority:p2 · domain:spec · type Feature. finding removed. 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

  2. huangyiirene commented on Sep 2, 2026

    @huangyiirene
    Collaborator

    Split filed: #14301 carries the build-time sandbox-lowering defect (the GLOBALS allowlist in packages/cli/src/utils/detect-free-identifiers.ts admits Intl, which the QuickJS sandbox does not provide), graded directly to pm:queue · domain:cli · p2. This card stays in the decision inbox for the feature question only.


    Generated by Claude Code

  3. os-project-manager commented on Sep 2, 2026

    @os-project-manager
    Collaborator

    Maintainer ruling recorded — A: field-level valueDomain, closed vocabulary = the existing three values

    Director 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. FieldSchema gains a valueDomain slot whose vocabulary is exactly SpecifierValueDomainSchema'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 (a format member) is not taken — format stays the offline shape-check family. Option C (prose only) is not taken. Adding Intl to the sandbox is out of scope; #14301 (the build-time lowering hazard) proceeds independently.

    Execution: domain:spec lane; Clause-②: yes (a published field schema widens its accept set), dispatch at CONTRACT_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

  4. claude commented on Sep 4, 2026

    @claude
    Contributor

    Claim: PM loop, domain:spec seat (seat post #6017), R5 of this shift — implementing the maintainer's ruling A (comment 5507503059, 「同意」 2026-09-02): FieldSchema gains a valueDomain slot whose vocabulary is exactly SpecifierValueDomainSchema'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 (GitHub zhuangjianguo)
    Branch: claude/issue-14168-field-value-domain
    Claimed at 2026-09-04T00:43Z; queue read 00:40Z (oldest ruled card in the lane's pm: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 under packages/spec/src/ (path chosen by measurement), their tests, the field error-code declaration the write path will emit (beside the maxLength / min family'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/spec changeset. ⛔ packages/services/** (service-settings' value-domains.ts re-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-review hung on this card in this stroke; the dev hangs it on the PR. Contract review at tier by this seat. H17: field.zod.ts is a declared Restart-touch trigger file of the v18 hold #8345 — notification posted there, no rider taken.


    Generated by Claude Code

  5. claude commented on Sep 4, 2026

    @claude
    Contributor

    Dispatch (R5 of this shift, 2026-09-04T01:39Z) — domain:spec seat, session_0174WZTU6XcFcS7g2kykC53i, seat post #6017. mode:subagent, model: fable (CONTRACT_REVIEW_TIER), size M, Clause ② yes (dual carrier: needs:contract-review hung on this card at claim; the dev hangs it on the PR). The dev leaves its own Claim: comment below carrying Clause-②: yes and 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, SpecifierValueDomainSchema keeps 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 against maxLength's list; the choice stated in the PR body), refused elsewhere at parse with a located issue via the maxLength / minLength applicability mechanism; (3) one exported membership predicate in spec — iana_time_zone = the Intl.DateTimeFormat probe (pins: UTC, Asia/Kolkata, Europe/Kyiv accepted; Europe/Munich refused), 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 the maxLength / min family'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/spec minor, 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

  6. 4 remaining items

  7. claude commented on Sep 4, 2026

    @claude
    Contributor

    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

  8. claude commented on Sep 4, 2026

    @claude
    Contributor

    Seat decision on PR #15133's CI red (surface, not contract) — widen by one test file, same PR. domain:spec PM seat, session session_0174WZTU6XcFcS7g2kykC53i (seat post #6017), 2026-09-04T02:50Z.

    CI on 18c6c7f4: Test Core (5/6) and Temporal 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 every FieldSchema key to be classified as storage or presentation in its own table; the new valueDomain key 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: classify valueDomain in that table beside maxLength / 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 in packages/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 HEAD after the last commit copied into the body and the report. Clause ② stays yes; needs:contract-review stays hung on both carriers until the review.

    The governed-surface reading (the eight skills/*/references/_index.md pointer lines) is a separate, read-only ask already sent to the dev: the mechanical check-governed-merges.mjs --test verdict at the head from a tree where the generator runs — the seat's own runs failed on an environment error (tsx not found under pnpm 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_CLASS from packages/drivers/driver-sql/src/builtin-column-collision.ts and says "Classify it in FIELD_KEY_STORAGE_CLASS". The one widened file is therefore that source module (one table entry for valueDomain, placed with the bounded-string family maxLength / minLength), not the test; the test file itself is left untouched. Claim reading corrected too: PR #11336 (open) touches builtin-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

  9. claude commented on Sep 4, 2026

    @claude
    Contributor

    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

  10. claude commented on Sep 4, 2026

    @claude
    Contributor

    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

  11. claude commented on Sep 4, 2026

    @claude
    Contributor

    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

  12. hotlong commented on Sep 4, 2026

    @hotlong
    Contributor

    Director audit of the contract — PASS (summon 13, session_01WXyGTWPbbreqXow7Z2pZCk; tier fuse read 2026-09-04T01:26Z, get_session served claude-fable-5-1 = CONTRACT_REVIEW_TIER). Subject: PR #15133 at head b6f10508a (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-member ValueDomainSchema, authorable on text only through the same superRefine door maxLength / minLength use, refused with a located issue naming the type elsewhere. Correct, and narrower than the bounded-string family for the reasons the docblock measures; select rightly excluded.
    • SpecifierValueDomainSchema becomes an identity alias of the shared schema (pinned toBe), same name, same members: importers do not move. Correct.
    • isValueDomainMember and ISO_3166_ALPHA2_CODES live in packages/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.
    • FieldErrorCode gains value_domain (ADR-0114 D1 naming, D2 new-kind rule) with templates in all four locales; the driver-sql storage-class entry is presentation, matching minLength. 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/spec minor, additive, no ADR-0087 marker owed. Accepted.

    ③ Boundary flags

    • The engine half (write-path refusal in record-validator.ts, ledger row to live, the two form rows) and the services half (re-point value-domains.ts onto the shared predicate) are the two follow-up cards the seat files at ACCEPT; each must carry Blocked-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_value to value_domain) belongs to the services card.

    Landing gate as of 03:15Z: mergeable_state: blocked with CI in progress on b6f10508a; the eight skills/*/references/_index.md lines 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

  13. claude commented on Sep 4, 2026

    @claude
    Contributor

    Contract review (Clause ②) on PR #15133 — PASS · ACCEPT: comment 5535158979 (2026-09-04T03:17Z), by the domain:spec PM seat, session session_0174WZTU6XcFcS7g2kykC53i, seat post #6017; tier fuse read 03:14Z, --pair 15133 exit 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 code value_domain on both doors, carried by the services card). needs:contract-review cleared on both carriers in this stroke. Landing at all-green on b6f10508 (18 checks running at 03:15Z); on MERGED this card closes via Fixes, pm:dispatched is stripped, landing note here, the engine / services / objectui follow-up cards are filed.


    Generated by Claude Code

  14. claude commented on Sep 4, 2026

    @claude
    Contributor

    Landed — PR #15133 merged via the queue at 2026-09-04T04:05:20Z, merge commit 1d7e76a6 (squash; origin/main tip 5c584231a at 04:06Z, the merge commit is its ancestor). domain:spec PM seat, session session_0174WZTU6XcFcS7g2kykC53i, seat post #6017. Contract review PASS · ACCEPT 5535158979 (director audit 5535155127 concurs); provenance 5535273829.

    Probed on origin/main at 04:06Z: shared/value-domain.zod.ts — ValueDomainSchema (:97), ISO_3166_ALPHA2_CODES (:123), isValueDomainMember (:175), re-exported from shared/index.ts:35; data/field.zod.ts — the valueDomain slot (:1022) and the applicability refinement (:1941-1944); system/settings-manifest.zod.ts:156 — SpecifierValueDomainSchema = ValueDomainSchema; api/errors.zod.ts:268 — value_domain; the liveness row planned (packages/spec/liveness/field.json:224); the driver-sql storage-class table carries valueDomain: 'presentation' (builtin-column-collision.ts:100, the one-file widening ruled in comment 5534967364).

    Card closed by the PR's Fixes; pm:dispatched stripped in this stroke (read back). The field.zod.ts / settings-manifest.zod.ts / shared/value-domain.zod.ts / errors.zod.ts / validation-message.ts reservation and the one-file builtin-column-collision.ts widening are released. Follow-up cards filed at 2026-09-04T04:10Z: #15161 (domain:engine write-path enforcement in record-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:services re-point of value-domains.ts onto @objectstack/spec/shared and value_domain on the settings door — pm:queue, domain:* triage's) and objectui#7597 (the Studio control; pm:on-hold with an installability Restart-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 code value_domain on both doors.


    Generated by Claude Code

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

Metadata

Metadata

Assignees

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions