Skip to content

feat(spec): Field.valueDomain, one closed standard-domain vocabulary and one membership predicate shared by settings specifiers and object fields - #15133

Merged
zhuangjianguo merged 7 commits into
mainfrom
claude/issue-14168-field-value-domain
Sep 4, 2026
Merged

feat(spec): Field.valueDomain, one closed standard-domain vocabulary and one membership predicate shared by settings specifiers and object fields#15133
zhuangjianguo merged 7 commits into
mainfrom
claude/issue-14168-field-value-domain

Conversation

@claude

@claude claude Bot commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

Fixes #14168

Spec half of maintainer ruling A (comment 5507503059, director seat, 2026-09-02, verbatim reply 「同意」). Dispatched by the domain:spec PM seat (session session_0174WZTU6XcFcS7g2kykC53i, seat post #6017); Clause ②: yes (a published field schema widens its accept set), contract review at tier.

The ruling, quoted where it is the operative criterion

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. Option C (prose only) is not taken. Adding Intl to the sandbox is out of scope.

This PR delivers the contract: the slot, the shared vocabulary, the shared predicate, the declared refusal (catalog member + translatable templates), the generated products and the docs. The write-path refusal itself (engine) and the settings door's re-point onto the shared predicate (services) are the follow-up cards the PM files at ACCEPT; #14238 sequences after this PR.

What changed

  1. Shared vocabularypackages/spec/src/shared/value-domain.zod.ts (new): ValueDomainSchema (the three members, in the same order), ValueDomain, isValueDomainMember(domain, value), ISO_3166_ALPHA2_CODES. Exported from @objectstack/spec/shared. SpecifierValueDomainSchema / SpecifierValueDomain in system/settings-manifest.zod.ts keep their names and are now an alias of the shared schema (expect(SpecifierValueDomainSchema).toBe(ValueDomainSchema) is pinned — identity, not a copy). No member added or removed; check:api-surface / check:export-origins / check:declaration-map regenerated (moved origin, unchanged shape).
  2. Field slotFieldSchema.valueDomain?: ValueDomainSchema with a .describe() stating what it constrains (membership of the WRITTEN value, the min/max/maxLength transition-gate class; a stored value is not re-read) and the applicability rule. Refused at parse on every other type with a located custom issue at [valueDomain] naming the type, through the same superRefine door maxLength / minLength use (VALUE_DOMAIN_FIELD_TYPES, the message enumerating the set itself).
  3. Shared predicateisValueDomainMember: iana_time_zone is the Intl.DateTimeFormat probe; iso_4217_currency is the key set of the package's own CLDR snapshot; iso_3166_alpha2 is the explicit 249-code list (exact uppercase). Details and the duplication window below.
  4. Refusal contractFieldErrorCode gains value_domain (ADR-0114 D1: the code is the property's own snake_case name, like max_length; D2: a new constraint KIND adds a catalog member), and validation-message.ts gains value_domain plus three per-domain rendering variants in all four platform locales (the completeness pins require every locale to carry every key).
  5. Generated products — api-surface, export-origins, declaration-map, authorable-surface (data/Field:valueDomain, shared/ValueDomain), json-schema manifest, reference docs (incl. the new references/shared/value-domain.mdx), liveness state counts, and the eight skills/*/references/_index.md pointer files (one generated line each, see Governed surface below). check:generated second run exits 0.
  6. Docsdata-modeling/fields.mdx, data-modeling/field-types.mdx, data-modeling/validation-rules.mdx (text table + summary), protocol/objectql/schema.mdx, api/error-catalog.mdx (field-level catalog table), getting-started/quick-reference.mdx (Shared Protocol total 7 to 8 — the curated table did not gain a row), packages/spec/llms.txt (206 to 207 schemas; shared 12 to 13, "Value Domain" named in the prose). content/docs/permissions/system-context.mdx was re-anchored by the census --fix (field.zod.ts line 1540 to 1580 — field.zod.ts sits above anchored declarations).
  7. Liveness ledgerliveness/field.json gains a planned row for valueDomain (evidence: the applicability door and the shared predicate; note states honestly that the record validator does not read the key yet and names the engine card as the flip to live).
  8. Changeset@objectstack/spec minor, additive, no BREAKING banner. Measured: check-adr-0087-registration judges only declared-breaking changesets (this PR adds no declared-breaking changeset (1 non-breaking changeset(s) seen)), so it expects no disposition marker on an additive key; the changeset carries the house-style not-required (accept-set expansion) comment anyway, as the carryOver precedent does.

Measurements

Applicability (the choice). BOUNDED_STRING_FIELD_TYPES (the maxLength / minLength family) has twelve members: text, textarea, email, url, phone, password, markdown, html, richtext, code, signature, qrcode. valueDomain takes the NARROWER family, text only. A domain member is one short identifier and the whole stored value is that identifier; of the twelve, only text stores a single plain string. The other eleven store a multi-line body (textarea, markdown, html, richtext, code), a value that already carries its own shape family (email, url, phone — a currency code is never an email), a masked credential (password, ADR-0100) or a data URI (signature, qrcode). select is deliberately excluded: its membership boundary is its exhaustive options table; the settings specifier's "options degrade to a suggestion list" semantics would be a second ruling, not a widening of this set. #14238's two timezone columns are text columns, so the narrower family serves the first consumer. Pinned: the three values accepted on text; a stranger (iso_8601_date) refused with invalid_value at [valueDomain], first sentence Invalid option, the three members listed; valueDomain on number (and on 18 other types, the eleven bounded-string siblings included) refused with custom at [valueDomain] naming the type; positive controls (absent key stays absent; domain composes with maxLength/minLength); the derived JSON schema carries the three-member enum.

Predicate home and definitions. packages/spec already ships pure shared verdicts over checked-in data (currencyPrecisionContradiction over the CLDR table, filterVerdict, the comparand-shape door), and the ruling names one predicate shared by two doors, so it lives beside the vocabulary. Prime Directive #2 is addressed in the module header: the predicate is pure, dependency-free, holds no state and reads no service. Definitions, each re-measured on node v22.22.2 by the shared test:

  • iana_time_zone = the Intl.DateTimeFormat probe. Pinned: UTC, Asia/Kolkata, Europe/Kyiv accepted (and all three absent from Intl.supportedValuesOf('timeZone'), which returns 418 and lists Asia/Calcutta instead); Europe/Munich and Mars/Olympus refused; europe/zurich accepted (the probe is case-insensitive — that is the pinned definition, unchanged from the settings door).
  • iso_4217_currency = the key set of data/currency-fraction-digits.ts (CURRENCY_FRACTION_DIGITS, CLDR 48.0 snapshot). Measured on the baseline: 162 keys, equal in both directions to Intl.supportedValuesOf('currency') (162) — the exact set the settings door enforces today. The test pins that equality as the drift detector, and pins the definition's known gaps (VED, XAU absent) rather than papering over them. Exact uppercase (usd refused), Object.prototype keys refused.
  • iso_3166_alpha2 = the explicit 249-code list, exact uppercase. Pinned: 249, each two uppercase letters; US/GB/CN/CH accepted; ZZ, UK, XX, AA, QM, QZ, EU, us refused; Intl.DisplayNames re-measured as NOT an oracle.

Duplication window and the services re-point. Until the services follow-up card lands, packages/services/service-settings/src/value-domains.ts carries its own copies. Not "fixed" here — spec must not import services, and services stays untouched by this card. The symbol-by-symbol re-point that card makes (value-domains.ts to @objectstack/spec/shared):

  • isIanaTimeZone(value) (module-private) → isValueDomainMember('iana_time_zone', value)
  • iso4217Codes() / the Intl.supportedValuesOf('currency') cache (module-private) → isValueDomainMember('iso_4217_currency', value) (same 162-code set; the cast for the ES2020 lib goes with it)
  • ISO_3166_ALPHA2 (module-private) and ISO_3166_ALPHA2_CODES (exported for its pins) → ISO_3166_ALPHA2_CODES from spec (same 249 codes; the structural pins in value-domains.test.ts re-point unchanged)
  • DOMAIN_MEMBERSHIP (the private record) → (value) => isValueDomainMember(domain, value) per member, or firstRejectedDomainMember calling the predicate directly
  • knownValueDomain, firstRejectedDomainMember, valueDomainPhrasing stay in services (door behaviour and prose, not membership); SpecifierValueDomain keeps resolving through @objectstack/spec/system.
    Whether the settings door's own refusal code moves from invalid_value to the new value_domain member is that card's question (open question 2 below).

Refusal contract (item 4 measurement). The family declares its codes in spec: FieldErrorCode in packages/spec/src/api/errors.zod.ts (min_length, max_length, min_value, max_value, max_scale), a closed catalog with no ledger tier, and the objectql write seam emits exactly those (packages/objectql/src/validation/record-validator.ts:570-571, fail('max_length', { maxLength: def.maxLength, actual: s.length }), building FieldValidationError { code: FieldErrorCode, message, label, constraint, value }). The settings door spells its domain refusal invalid_value with constraint: { valueDomain } (settings-service.ts:1808-1832 docblock, :1024-1035 env path). Per ADR-0114 D2 ("a new kind of constraint is a type-system change and deserves a line in the spec"), this PR declares value_domain beside the family, so the engine card implements a declared contract: fail('value_domain', { valueDomain: def.valueDomain, actual: s }, 'value_domain_' + def.valueDomain, undefined, s) renders through the shipped templates.

Governed surface — certified regeneration only

The diff touches skills/*/references/_index.md (eight files, one generated pointer line each: every skill index enumerates the shared/ modules, so the new module appears in each). Pure regeneration by pnpm --filter @objectstack/spec gen:skill-refs, byte-exact against this tree (check:skill-refs green inside check:generated); no hand-authored file under skills/**. Per Prime Directive #14 and the maintainer's 2026-09-01 ruling the queue guard lifts a certified pure regeneration with zero approvals; the PM decides the landing.

Deviations and things to know

  • File surface vs the PM claim: adds system/validation-message.ts (+ test), the ADR-0122 pin file (type-alias-convention.pin.test.ts: Iso867 for the new bare alias; the pin count 829 to 830 with its recorded reason), settings-manifest.test.ts, packages/spec/llms.txt, quick-reference.mdx, error-catalog.mdx, field-types.mdx, and the census re-anchor — each forced by a gate named in the verification table. Not touched: packages/services/**, packages/objectql/**, packages/core/**, objectui, content/docs/releases/**.
  • The two spec authoring forms (data/field.form.ts, data/object.form.ts) are hand-written visibleWhen lists that show maxLength/minLength for exactly the enforced set. They are NOT edited here: showing valueDomain in Studio's field form before the write path enforces it would advertise an inert capability (ADR-0078 / Prime Directive chore: version packages #10). The engine card adds the two form rows in the same stroke it flips the ledger row to live (open question 1).
  • origin/main was merged before opening (one overlapping file, system-context.mdx, regenerated by the census on the merged tree; the deferred regeneration was discharged in its own commit as pre-commit requires).
  • The describe() text names no issue ids (check:doc-authoring clean: 14725 customer-facing strings); ADR ids are cited where a decision is load-bearing.
  • H17: field.zod.ts is a declared trigger file of the v18 hold currency: no exchange-rate / home-currency conversion — dynamic multi-currency amounts cannot be reconciled or aggregated across currencies #8345; no rider taken. The hot-file diff is the import, the applicability set, the slot and the superRefine block.

Read-and-report for the follow-up cards (no edits made)

(a) Engine. packages/objectql/src/validation/record-validator.ts: FieldDef interface :156-172 (add valueDomain?: string); the bounded-string branch :568-583 reads def.maxLength / def.minLength and returns fail('max_length', { maxLength, actual }) / fail('min_length', ...); fail is :486-492 (code: FieldErrorCode, constraint?, messageKey?, options?, value?) building through buildFieldError (:292-310, value rides the interpolation namespace so a template can say {{value}}). The valueDomain check belongs in that branch after the length checks, judged only when t === 'text' (the applicability set), calling isValueDomainMember(def.valueDomain, s) from @objectstack/spec/shared. packages/core enforces nothing here; packages/core/src/security/resolve-authz-context.ts:936 carries a module-private isValidTimeZone with the same probe (a later re-point candidate, not this ruling's).
(b) Services. The symbol-by-symbol re-point is listed above under Duplication window.
(c) objectui. Not generic. The Studio field editor is the hand-written inspector packages/app-shell/src/views/metadata-admin/inspectors/ObjectFieldInspector.tsx (:626-641: isTexty(type) gates two InspectorNumberFields bound to def.minLength / def.maxLength, i18n keys designer.field.minLength / .maxLength in metadata-admin/i18n.ts:1403,3297); the generic JSON-schema form SchemaForm.tsx (:4-9, renders /meta/types schema) explicitly does not cover Object/Field designers (:20-23), and mergeServerFields.ts:30-32 states "SchemaForm renders ONLY the fields a form declares". So the new key reaches OpenAPI and the /meta/types schema without a UI change, but a Studio control for it is an objectui edit (a select bound to def.valueDomain, shown when type === 'text'), sequenced with the engine card. Measured at objectui a27d153c.

Patch round — surface widened by one source module (seat ruling, comment 5534967364)

CI on 18c6c7f49 was red in Test Core (5/6) and Temporal Conformance (live PG + MySQL) with one failing test: packages/drivers/driver-sql/src/builtin-column-collision.test.ts › "classifies EVERY FieldSchema key, and invents none" — the pin that requires every FieldSchema key to be classified storage or presentation in FIELD_KEY_STORAGE_CLASS. The domain:spec seat ruled (comment 5534967364, with its in-place correction) to widen this card's surface by exactly that one SOURCE module, packages/drivers/driver-sql/src/builtin-column-collision.ts, on the four-condition adjacent-fix path; the test file is untouched (PR #11336 touches it) and nothing else under packages/drivers/** moves.

The 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 (varchar(n) vs TEXT), minLength is 'presentation' as write-time validation. valueDomain is write-time membership validation of the WRITTEN string against a standard; createColumn never reads it and the column stays the string maxLength sizes, so the DDL has nothing to discard — the minLength class. The docblock makes no case for 'storage'. No sibling landing on origin/main touches the module since this branch's base (git log f594e70d..origin/main -- <module> is empty).

Before / after readings of that test file (same worktree, driver-sql closure built, under the lock, exits captured by redirect):

Reading Result
BEFORE the entry (at 18c6c7f49) exit 1 · Tests 1 failed, 10 passed (11) · AssertionError: unclassified FieldSchema key(s): expected [ 'valueDomain' ] to deeply equal []
AFTER the entry (at 750d24603) 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 (and ✓ … 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 at 750d24603 exit 0 · All 15 generated artifacts are up to date.
spec pin set (the seven files) under the lock at 750d24603 Test Files 7 passed (7) · Tests 416 passed (416) · VERDICT command-exit 0
node scripts/check-system-context-census.mjs at 750d24603 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.

Final head after the patch round: 750d24603. Merge round (GitHub read the PR mergeable_state: dirty against origin/main fcc42e6c1, so no pull_request CI ran on 750d24603): origin/main was merged as merge commit ef6772673 via scripts/pm/os-regen-merge.sh, and the two artifacts the os-regen driver deferred — content/docs/references/index.mdx (schema totals 1590 to 1593, automation 69 to 72, main's additions) and content/docs/permissions/system-context.mdx (one anchor re-derived, protocol.ts:1747 to :1795, main's move) — were regenerated on the merged tree by the repo's own generators (check:generated --fix = gen:schema + gen:docs; gen:system-context-census) in regeneration commit b6f10508a, whose staged diff was exactly those two files; second check:generated run exits 0 (All 15 generated artifacts are up to date.). Final head: b6f10508a (pushed fast-forward; no rebase, amend or force; origin/main is an ancestor). At b6f10508a: spec build 34/34 d.ts present; spec typecheck exit 0 (check:test-typecheck: OK — 54 file(s) / 261 error(s) / 145 pinned signature(s)); spec pin set Test Files 7 passed (7) / Tests 416 passed (416); driver-sql classification test Tests 11 passed (11); check:strictness-ledger exit 0 (counts.md is current — 440 site(s) measured); census check exit 0 (106 elevation read sites ... 140 anchors resolve); 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 / ✅ NOT governed, all eight lifted under the generated-surface exception (#11705). No behaviour changed in either round. needs:contract-review stays hung on both carriers.

Verification (head 18c6c7f49 for the spec half — the table below was taken there; the patch round above cites 750d24603; exits captured before any pipe; verdict lines quoted)

Check Result
pnpm --filter @objectstack/spec build (lock) check-dts-emitted: @objectstack/spec - 34/34 declared declaration file(s) present. · VERDICT command-exit 0
vitest: shared/value-domain.test.ts, data/field.test.ts, system/settings-manifest.test.ts, api/errors.test.ts, system/validation-message.test.ts, type-alias-convention.pin.test.ts, data/currency-precision-iso4217.test.ts (lock) Test Files 7 passed (7) · Tests 416 passed (416) · VERDICT command-exit 0
pnpm --filter @objectstack/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
pnpm --filter @objectstack/spec 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, no output
pnpm check:doc-authoring exit 0 · 14725 customer-facing string(s) across 724 spec sources clean — no internal issue-id references
pnpm check:nul-bytes exit 0 · check-nul-bytes: OK (scanned 8229 text file(s) ...)
check-adr-0087-registration --base 2cc4610 / check-changeset-no-major --base 2cc4610 exit 0 · this PR adds no declared-breaking changeset (1 non-breaking changeset(s) seen) · This diff introduces no major bump.
@objectstack/lint closure build + typecheck + vitest (lock) Test Files 94 passed (94) · Tests 2900 passed, 5 skipped (2905) · VERDICT command-exit 0
check:error-code-provenance, check:exported-any, check:dual-source-exports, check:browser-reachable-entries, check:entry-nameability, check:empty-state, check:strictness-ledger, check:variant-docs, check:yaml-examples, check:llms-txt, check:quick-reference-counts, check:spec-parsed-alias, check:error-code-casing, check:system-context-census, check:type-check-coverage, lint check:doc-formula-expressions / check:doc-security-posture all exit 0
node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands (89 commands, derived at 18c6c7f49, identical to the pre-merge derivation) 85 exit 0 at this tree's content; 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 four need the full monorepo build or objectui's manifest — CI's run.
Reverse verification (from committed state ab05a825a, restore leg proven) Removed the valueDomain: slot line (grep -c 1 to 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 the positive controls that do not read the slot). Restore: git checkout HEAD -- path; git hash-object = HEAD: blob 5b6f87db72c19cb856e1e980a8f05c4e3ac6e7f0 both sides; git diff HEAD --stat empty; git status --porcelain empty. No build in the resolution path: field.test.ts imports ./field.zod as relative source, not through dist/.

Generated by Claude Code


Generated by Claude Code


Generated by Claude Code

ObjectStack Dev Agent and others added 4 commits September 4, 2026 02:00
…d predicate (#14168)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0174WZTU6XcFcS7g2kykC53i
… and one membership predicate shared by settings and fields (#14168)

Maintainer ruling 2026-09-02 (option A, spec half): FieldSchema gains a
`valueDomain` slot whose vocabulary is exactly SpecifierValueDomainSchema's
three members; the vocabulary and the membership predicate move to
shared/value-domain.zod.ts and both the settings specifier and the field
slot reference them. The write-path refusal (engine) and the settings
door's re-point (services) are the follow-up cards.

- shared/value-domain.zod.ts: ValueDomainSchema, ValueDomain,
  isValueDomainMember, ISO_3166_ALPHA2_CODES (+ test)
- settings-manifest.zod.ts: SpecifierValueDomainSchema is now an alias of
  the shared schema (same name, same three members, same shape)
- field.zod.ts: the slot, VALUE_DOMAIN_FIELD_TYPES (text only — measured
  narrower than the maxLength family), the superRefine applicability refusal
- api/errors.zod.ts: FieldErrorCode member `value_domain` (ADR-0114 D1/D2)
- system/validation-message.ts: four-locale templates for value_domain and
  its three per-domain variants
- liveness/field.json: `planned` row for the new authorable key
- generated: api-surface, export-origins, declaration-map,
  authorable-surface, json-schema manifest, reference docs, liveness counts,
  skill reference indexes (pure regeneration)
- hand-written docs: fields, field-types, validation-rules, objectql schema,
  error-catalog, quick-reference count, llms.txt inventory count
- changeset: @objectstack/spec minor

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0174WZTU6XcFcS7g2kykC53i
… merging main (#14168)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0174WZTU6XcFcS7g2kykC53i
@github-actions

github-actions Bot commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/driver-sql, @objectstack/spec, touching 22 documentable anchor(s). ⚠️ 11 changed file(s) yielded no anchor (packages/spec/api-surface/data.json, packages/spec/api-surface/shared.json, packages/spec/authorable-surface/data.json, …), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

17 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: node scripts/docs-audit/affected-docs.mjs --json fcc42e6c1de9df5c65ab60166bf15c3aeea5bfe2.

1 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails.

What this run could not see
  • 11 changed file(s) yielded no anchor (packages/spec/api-surface/data.json, packages/spec/api-surface/shared.json, packages/spec/authorable-surface/data.json, …) — pages documenting those are invisible to this run
  • the SDK route bridge reached 47 of 219 client-bound route-ledger rows — the other 172 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 172: 14 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 56 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 102 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 131 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json fcc42e6c1de9df5c65ab60166bf15c3aeea5bfe2packageMentionDocs.

Which tree this was computed on

This run read content/docs from e45d15e6bcb50431a3115f98ee2c5f31a73c362c — the merge of head b6f10508ad27d3447d160e8339f1b58e7d4a7d2f into base fcc42e6c1de9df5c65ab60166bf15c3aeea5bfe2, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin e45d15e6bcb50431a3115f98ee2c5f31a73c362c && git checkout e45d15e6bcb50431a3115f98ee2c5f31a73c362c
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin fcc42e6c1de9df5c65ab60166bf15c3aeea5bfe2 b6f10508ad27d3447d160e8339f1b58e7d4a7d2f && git checkout -B drift-repro fcc42e6c1de9df5c65ab60166bf15c3aeea5bfe2 && git merge --no-ff b6f10508ad27d3447d160e8339f1b58e7d4a7d2f

node scripts/docs-audit/affected-docs.mjs --json fcc42e6c1de9df5c65ab60166bf15c3aeea5bfe2

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs fcc42e6c1de9df5c65ab60166bf15c3aeea5bfe2 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@claude

claude Bot commented Sep 4, 2026

Copy link
Copy Markdown
Contributor Author

CI on 18c6c7f4 (2026-09-04T02:52Z, domain:spec PM seat, session session_0174WZTU6XcFcS7g2kykC53i): 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 › "classifies EVERY FieldSchema key, and invents none": unclassified FieldSchema key(s): expected [ 'valueDomain' ] to deeply equal [] (jobs 100892278254 / 100892278038). This PR's: the new FieldSchema.valueDomain key must be classified in FIELD_KEY_STORAGE_CLASS (packages/drivers/driver-sql/src/builtin-column-collision.ts:72), which no spec-only shape can do. Seat ruling on #14168 (comment 5534967364): widen the surface by that one source file, one table entry beside the bounded-string family; the fix round is with the dev now, a fast-forward push follows. The governed-surface reading for the eight skills/*/references/_index.md pointer lines is in: check-governed-merges.mjs --test at this head exits 0, all eight lifted as a pure regeneration (#11705) — ordinary queue landing applies.


Generated by Claude Code

ObjectStack Dev Agent and others added 3 commits September 4, 2026 02:58
…KEY_STORAGE_CLASS (#14168)

The builtin-column-collision pin requires every FieldSchema key to be
classified storage or presentation. `valueDomain` is write-time membership
validation of the written string against a standard — never read by
`createColumn`; the column stays the string `maxLength` sizes — so it joins
`minLength` on the presentation side. Widening ruled by the domain:spec seat
(comment 5534967364 on #14168); the test file is untouched.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0174WZTU6XcFcS7g2kykC53i
…t census on the merged tree (#14168)

Regeneration commit after merging origin/main (merge commit ef67726): the
os-regen driver deferred content/docs/references/index.mdx and
content/docs/permissions/system-context.mdx; both are re-derived here from
the merged tree by the repo's own generators (gen:schema + gen:docs via
check:generated --fix; gen:system-context-census). No behaviour change.

Claude-Session: https://claude.ai/code/session_0174WZTU6XcFcS7g2kykC53i
Co-authored-by: Claude <noreply@anthropic.com>
@claude

claude Bot commented Sep 4, 2026

Copy link
Copy Markdown
Contributor Author

Contract review (Clause ②) — PASS · ACCEPT

Reviewer of record: the domain:spec PM seat, session session_0174WZTU6XcFcS7g2kykC53i (seat post #6017). Tier fuse: get_session read at 2026-09-04T03:02Z and again 03:14Z — both model fields equal CONTRACT_REVIEW_TIER. Gate: check-clause2-carriers.mjs --pair 15133 exit 0 at 03:15Z (the Clause-②: declaration is in the dev's own claim comment 5534542455, edited in place at 03:00Z with the widened file; both carriers hung). Reports read against GitHub and the fetched branch: 5534923686 (02:44Z, 18c6c7f4), 5534967664 (02:50Z, the governed reading), 5535044765 (03:01Z, 750d2460); the merge-round report for b6f10508 is pending — the merge delta below was verified by this seat.

Head and window (readings at 2026-09-04T03:17Z)

The widening, verified in the diff

  • One vocabulary by reference: ValueDomainSchema = z.enum(['iana_time_zone', 'iso_4217_currency', 'iso_3166_alpha2']) in the new packages/spec/src/shared/value-domain.zod.ts; SpecifierValueDomainSchema = ValueDomainSchema (settings-manifest.zod.ts:156) — same export name, same members, an identity alias. No member added or removed; pinned ("refuses a stranger by name — the vocabulary does not widen").
  • The slot: FieldSchema.valueDomain?: ValueDomainSchema with a describe stating write-path membership of the WRITTEN value, the transition-gate class; applicability VALUE_DOMAIN_FIELD_TYPES = { 'text' } — measured narrower than the twelve-type maxLength family with a per-type reason (bodies, shape families, credentials, data URIs) and select excluded consciously (its boundary is its options table; importing the specifier's demote-to-suggestion semantics is a second ruling); refused elsewhere at parse through the family's superRefine door with code: 'custom' at [valueDomain] naming the type. Seat accepts the narrower set as the ruling's honest reading — a wider family is additive later.
  • One predicate in spec: isValueDomainMember(domain, value)iana_time_zone = the Intl.DateTimeFormat probe (pins: UTC, Asia/Kolkata, Europe/Kyiv accepted; Europe/Munich refused; case-insensitivity pinned as the definition); iso_4217_currency = the key set of the checked-in CLDR snapshot (162 codes, pinned equal both ways to Intl.supportedValuesOf('currency') on the Node baseline — the drift detector); iso_3166_alpha2 = the explicit 249-code list (count, spelling, no duplicates, reserved/alias elements refused, Intl.DisplayNames pinned as NOT an oracle). The module header records why a pure, dependency-free verdict belongs in the spec beside currencyPrecisionContradiction.
  • The refusal contract, declared where the family declares it: FieldErrorCode gains value_domain (api/errors.zod.ts:268, ADR-0114 D1/D2) with four-locale message templates in system/validation-message.ts — the engine's write-path half implements a declared code. Liveness: a planned row for field.valueDomain naming the parse-time door as its evidence and the engine card as the flip.
  • The A2-style widening (seat ruling 5534967364 with its in-place correction): exactly one file outside the declared surface, packages/drivers/driver-sql/src/builtin-column-collision.tsvalueDomain: 'presentation' in FIELD_KEY_STORAGE_CLASS beside minLength (write-time validation of the written string; createColumn never reads it, the column stays what maxLength sizes). Four-condition check: same defect class (a FieldSchema-key ratchet learning the ruled key), mechanical (the assertion names the key and the table), the module is claimed by no one (0 of 26 open PRs at 02:51Z; the test file that PR chore: version packages #11336 — the changesets release PR — touches was left alone), same gate family (Test Core / Temporal). The ratchet's before/after readings: 1 failed | 10 passed at 18c6c7f411 passed (11) at 750d2460.
  • Preservation, pinned: absent valueDomain stays absent on every type (no default materializes); maxLength composes with it; the derived JSON schema carries the enum (the Studio / OpenAPI / form-layer path); settings-manifest pins unchanged in meaning.

Report checklist

  • Reverse verification: one leg from the committed state — the slot line removed (grep 1→0), field.test.ts -t valueDomain23 failed, 2 passed (the two survivors are positive controls that do not read the slot); restore proven by git hash-object = HEAD: blob and empty git diff HEAD / porcelain. Direction red, as expected.
  • Gate readings at 18c6c7f4 / 750d2460 (unchanged by the merge round): spec build (check-dts-emitted 34/34), 7 pin files 416/416, spec typecheck + check:test-typecheck ledger unchanged (261 / 145), check:generated clean second run (liveness "field 93 classified (live 89, planned 3, dead 1)"), eslint 0 over the 12 edited TS files, doc-authoring clean, nul-bytes OK, no-major green, ADR-0087 gate "no declared-breaking changeset" (an additive key needs no marker; the house-style not-required (accept-set expansion) comment is present anyway), downstream @objectstack/lint typecheck + 2900 tests green, @objectstack/driver-sql test file 11/11 + typecheck 0, census re-anchored by --fix; dispatch-gates --commands 89 derived, 85 exit 0, NOT MEASURED by their own words: dual-build CJS loads, type-check debt, skill-examples (client-react dist), react-declaration-parity (objectui manifest) — all CI-owned.
  • Changeset: @objectstack/spec published, changeset present — minor, additive; SpecifierValueDomainSchema's alias noted in the ADR-0087 comment. Accepted.
  • Docs: six hand-written pages + the new references/shared/value-domain.mdx; the docs-drift advisory's 17 named pages are the symbol-level hits — the seat spot-checked field-types.mdx, fields.mdx, validation-rules.mdx, error-catalog.mdx, quick-reference.mdx and the census-anchored permissions/system-context.mdx are all in the file list; the one release-owned page it named is read-only and untouched.
  • Scope: 47 files = the declared surface + the additions the gates forced (each named in the PR body's deviations) + the one driver-sql entry + generated followers; no packages/services/**, packages/objectql/**, packages/core/**, objectui edits; the two spec authoring forms (field.form.ts / object.form.ts) deliberately untouched — see ruling 1 below. Closing keyword, two reads (03:02Z): first line Fixes #14168; no other closing keyword beside a card number. Fixes is correct — the card is the spec half; the halves below are new cards.
  • MCP-call accounting: 2 GitHub MCP search_issues (the REST search endpoint refuses this session — declared) + 1 subscribe_pr_activity; noted, not a finding. Head pinning: the reports' heads = the branch heads they name; the merge delta is the seat's own reading.
  • Commit trailers: 750d2460 carries the harness-written Co-Authored-By line — the dispatch runbook (dispatch-runbook.md:247) records that trailer as exempt from the maintainer's 2026-09-03 model-identifier rule; recorded on finding(skills): AGENTS.md prescribes no commit-message trailer for agent commits — the model-free Claude-Session: + Co-authored-by: Claude pair exists only in git log #15125.

Seat rulings on the report's open questions (open to the maintainer's veto)

  1. The two authoring-form rows (field.form.ts / object.form.ts visibleWhen for valueDomain on text): A — the engine card adds them in the same stroke that flips the liveness row planned → live (declared = enforced = shown; a key shown in Studio before any write path enforces it is the ADR-0078 shape).
  2. The settings door's refusal code once services re-points onto the shared predicate: A — one code, value_domain, on both doors (the ruling is one vocabulary and one predicate for both; the catalog's rule is that the code is the constraint's own name); the services card carries it with a pin — a wire-visible change on the settings door, stated there.

Out-of-scope group (sweep criterion: copies of the membership predicate and consumers of the key outside the spec)

Follow-up cards this seat files at landing

  1. domain:engine — the write-path enforcement: isValueDomainMember on the written value for a text field declaring valueDomain, refused with value_domain (constraint: { valueDomain }), the two authoring-form rows, the liveness row planned → live; read-and-report from the dev: record-validator.ts:568-583 (string branch), fail() at :486, FieldDef at :156-172. No platform object carries a timezone, so every app that computes a date boundary has to invent one — and each will invent it differently #14238 (ruled A, two timezone columns) sequences after it.
  2. domain:services — re-point service-settings/src/value-domains.ts (isIanaTimeZone, iso4217Codes, ISO_3166_ALPHA2 / ISO_3166_ALPHA2_CODES, DOMAIN_MEMBERSHIP) onto @objectstack/spec/shared, deleting the copies, and switch the settings door's refusal to value_domain with a pin (ruling 2 above).
  3. objectui — a Studio control for valueDomain on a text field: the field inspector is a hand-written key list (ObjectFieldInspector.tsx:626-641; SchemaForm excludes the Object/Field designers), consumer-side and release-gated.

Landing

At all-green on b6f10508: provenance → ready → squash auto-merge → added_to_merge_queue. On MERGED: strip pm:dispatched, probe shared/value-domain.zod.ts / data/field.zod.ts / settings-manifest.zod.ts:156 on origin/main, landing note, the three cards above filed, H17 landing notice on #8345.

needs:contract-review is cleared on this PR and on #14168 in this stroke (read-modify-write, both read back). If the head moves after this clear, the gate's C3 reading re-hangs it and the delta is reviewed before it is cleared again.


Generated by Claude Code

@claude

claude Bot commented Sep 4, 2026

Copy link
Copy Markdown
Contributor Author

Landing provenancedomain:spec PM seat, session session_0174WZTU6XcFcS7g2kykC53i (seat post #6017). Contract review (Clause ②) PASS · ACCEPT: comment 5535158979; needs:contract-review cleared on both carriers in that stroke.

Landing window readings, all on head b6f10508 at 2026-09-04T03:33Z (latest run per check name):

  • CI: all green — {('completed', 'success'): 30, ('completed', 'skipped'): 4} (0 red, 0 running); the required floor (Lint & Repo Gates, Type Check · workspace / · consumer gates / · debt ledger, Check Changeset, Build Core, Temporal Conformance (live PG + MySQL)) all success on this head.
  • Trial merge onto origin/main 1876d5dfd: clean.
  • check-clause2-carriers.mjs --pair 15133: exit 0 (head unchanged since the clear — no C3). No content/docs/releases/ edits.
  • Closing keyword, two reads: first line Fixes #14168; no other closing keyword beside a card number in the body (read at review time; body unchanged since).

Sequence: ready → squash auto-merge → added_to_merge_queue. On MERGED: strip pm:dispatched from #14168, probe on origin/main, landing note on the card, the follow-up cards named in the review filed.


Generated by Claude Code

@zhuangjianguo
zhuangjianguo marked this pull request as ready for review September 4, 2026 03:33
@zhuangjianguo
zhuangjianguo added this pull request to the merge queue Sep 4, 2026
Merged via the queue into main with commit 1d7e76a Sep 4, 2026
42 checks passed
@zhuangjianguo
zhuangjianguo deleted the claude/issue-14168-field-value-domain branch September 4, 2026 04:05
zhuangjianguo pushed a commit that referenced this pull request Sep 4, 2026
…s liveness row live

Seat ruling 1 on PR #15133: the engine card adds the two form rows in the same
stroke that flips the liveness row `planned` to `live` — declared = enforced =
shown. Both rows' `visibleWhen` mirrors `VALUE_DOMAIN_FIELD_TYPES`; the object
form's choices are derived from the vocabulary rather than re-typed, so the
control cannot become a second opinion on what the closed vocabulary is.

The liveness row now cites the record-validator seam beside `maxLength`'s.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ARYe3yQTQCUFm5qPYNgKaJ
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

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

2 participants