Skip to content

fix(spec): discriminate FieldSchema.defaultValue (literal / runtime token / CEL envelope) and validate each shape on its own terms - #7403

Merged
os-zhuang merged 3 commits into
mainfrom
claude/issue-7127-default-value-discriminator
Aug 10, 2026
Merged

fix(spec): discriminate FieldSchema.defaultValue (literal / runtime token / CEL envelope) and validate each shape on its own terms#7403
os-zhuang merged 3 commits into
mainfrom
claude/issue-7127-default-value-discriminator

Conversation

@os-zhuang

@os-zhuang os-zhuang commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

Closes #7127.

Implements the maintainer's ruling on the issue (comment 5236142227, 2026-08-10), verbatim and untranslated in its directive part:

Maintainer ruling (2026-08-10, directed in session session_01BPWqbmEFU8gJepBJTHESXd): sequenced, not chosen.

First land the literal / runtime-token / CEL vocabulary discriminator (shared with the action-param z.unknown() hole this card cites — one discriminator, two consumers). Then narrow FieldSchema.defaultValue using it. No interim tolerance and no widening while the discriminator is pending; the spec seat slices the two steps as it sees fit.

One PR, two commits in ruling order (the spec seat's recorded slicing decision, claim comment 5237312268), plus a merge of main:

  1. Commit 1 — the discriminator (packages/spec/src/data/default-value-shape.ts): the engine's envelope predicate verbatim (ObjectQL.applyFieldDefaults — truthy dialect + string source), the spec token predicates, discrimination in the engine's own order (envelope → token → literal), and checkLiteralDefaultValue — the shared literal-vs-stored-contract core (ADR-0104 D1 valueSchemaFor(def, 'stored')). The An action param's defaultValue is typed z.unknown(), so a default that can never satisfy the param's own value contract is accepted at authoring time and only 400s at submit #6970 action-param refinement in ui/action.zod.ts is refactored onto the shared core with zero behavior change — its pins (action-param-default-value.test.ts) stayed green unedited through the whole PR. Presence deliberately stays with each consumer (engine: '' is a real default; action params: blank is absent) — recorded in the module note.
  2. Commit 2 — the narrowing: FieldSchema's existing superRefine (the ADR-0113 block) is extended — no second refinement — with: absent (dv == null) skip → envelope structural acceptance → token per-token-per-type table → literal through the shared core. Plus tests, changeset, regenerated artifacts.
  3. Merge of origin/main (base moved while the PR opened): one content conflict in content/docs/protocol/objectql/schema.mdx, resolved by taking main's side — see the docs note below. ui/action.zod.ts auto-merged (main's fix(spec): prescribe per action type when object-form params is refused — bodyExtra for api, target interpolation + openIn for url (#6828) #7375/refactor(spec)!: retire global_nav from ACTION_LOCATIONS — a location the product never rendered, and the designer previewed anyway (#6888) #7322 changes + this PR's refactor coexist; pins re-run green on the merged tree). Generated artifacts regenerated from the merged tree after a fresh spec build (the pre-commit os-regen debt hook enforced exactly this), check:generated 11/11.

Premise re-verification (issue rule 6)

Re-probed on the pre-merge origin/main (f188ed6) before any edit: all 11 dossier acceptance rows reproduce — every one ACCEPTED by the pre-change schema, including number + "abc", text + NOW(), user + current_user, a CEL envelope, an arbitrary object, and a function. (One probe row initially rejected on options.0.value — my probe's own invalid option value, corrected and re-measured as ACCEPT.) Premise holds.

Gating table as implemented (dossier §3, with the recorded Q1–Q5 adjudications)

shape recognised by verdict
absent dv == null (engine applyFieldDefaults; '' is PRESENT) skipped
CEL envelope truthy dialect + string source — the engine's predicate verbatim structural acceptance only; result type is an ADR-0032 runtime concern
token NOW() isNowDefaultToken (case-insensitive, trim) legal on datetime, date, time (Q1 in — both resolvers and types.mdx support all three); refused elsewhere incl. text (Q2 — the silent instant-into-text interception "nobody chose" is refused, prescription names the temporal types); refused on any multi-value field (Q3 — both tokens resolve to one scalar)
token current_user exact match legal on user, and lookup with reference: 'sys_user' only (Q4 strict, matching the #4560 failure mode); refused elsewhere and on multi-value
literal everything else valueSchemaFor(def, 'stored') — the #6970 mechanics one layer down; near-miss token spellings ('now', '{current_user}', …) are SUGGESTED inside refusals, never accepted as tokens (Q5; default-value-tokens.ts's recorded refusal to widen)

Refusals follow the self-prescribing house style (field name, type, offending value verbatim, why, legal alternatives), pinned by path + code + message content in field-default-value.test.ts (37 cases), mirroring the #6970 pin table.

The §2d ESM cycle — discharged structurally, verified by real build

field-value.zod.ts dereferenced AddressSchema (imported from field.zod.ts) at module-eval time, so a field.zod → field-value.zod runtime edge would have been an evaluation cycle with an order-dependent TDZ crash. Resolution: AddressSchema MOVED into field-value.zod.ts (it IS the enforced address value contract there) and is re-exported from field.zod.ts unchanged — the back-edge becomes the type-only FieldType import, erased at runtime. This is the dossier's sanctioned escape, at minimum width: no import site outside the two files changes, the public surface is identical, only the export ORIGIN moves (both snapshots regenerated). Discharged by: full pnpm --filter @objectstack/spec build including the DTS pass (exit 0) + a dist-eval smoke test (dist/data/index.mjs and root dist/index.mjs both evaluate; gate live; AddressSchema/AddressValueSchema parse correctly from dist).

Census — migration surface (hard constraint: 0 shipped refusals)

Re-ran the dossier's census approach by executing the implemented gate's branches (imported from this branch's source) over every object module. Cloud WAS locally available (/home/user/cloud @ 485cbd3 — the dossier's exact ref), censused via a dedicated read-only worktree with a throwaway spec symlink, removed afterwards.

corpus modules declarations literal NOW() current_user envelope refusals
objectstack (this branch) 116 244 159 84 (all datetime) 1 (user) 0 0
cloud @ 485cbd3 37 127 80 47 (all datetime) 0 0 0
total 153 371 239 131 1 0 0

Matches the decision dossier's table exactly, after deduping barrel re-exports by object identity. 2 failed imports, both create-objectstack scaffold templates with no resolvable deps — hand-checked: they declare no defaultValue at all.

The two docs-sample defects — fixed on main first, no longer in this diff

The claim comment sanctioned fixing schema.mdx's two defective samples (bare CEL source string; currency object literal) in this PR, and commit 2 originally did. While this PR was opening, an independent docs-only change landed the same two fixes on main; the merge conflict was exactly that collision and was resolved by taking main's text (semantically identical to mine). This PR's effective diff therefore no longer touches content/docs/protocol/objectql/schema.mdx — both samples are already correct on the base, and the gate this PR adds is what refuses their old forms.

Reverse verification — predicted BEFORE running, then measured

Prediction (recorded in writing first): reverting ONLY the field-gate block leaves 21 tests red — the 18 rejection-class table rows + 3 standalone pins — with all acceptance rows, the discriminator suite, and the action-param suite green; direction plain red, explicitly NOT the #5009 inversion (there was no pre-existing rule to over-reach; removal can only widen acceptance).

Measured: field-default-value.test.ts37 tests | 21 failed, exactly the predicted 21; default-value-shape.test.ts + action-param-default-value.test.ts fully green. Predicted-vs-measured 21/21, no surprises. Gate restored via git checkout; no stash used anywhere.

Gates and evidence

check result
pnpm --filter @objectstack/spec build (incl. DTS) exit 0 pre-merge AND re-run exit 0 on the merged tree, .build-input-hash stamped
full spec suite 364 files / 9532 tests — all green (pre-merge head) after one honest pin update: strictness-ledger.test.ts pins field-value.zod.ts's site count, which the AddressSchema move changed 2 → 3 (comment updated in the pin, same style as its chart.zod.ts sibling)
pin suites re-run on the MERGED tree (field gate + discriminator + action-param + strictness ledger) 4 files / 104 tests green
pnpm --filter @objectstack/spec typecheck exit 0 pre-merge and on the merged tree (incl. test-layer ledger check)
pnpm --filter @objectstack/platform-objects test (largest defaultValue consumer) 12 files / 296 tests green (after building its deps — the fresh-worktree trap, not a regression)
check:generated 11/11 current on the merged tree; regenerated only the proved-stale set each round (api-surface + export-origins dual snapshot after the real build, docs references, skill refs, strictness ledger)
node scripts/check-adr-0087-registration.mjs --base origin/main green — "this PR adds no declared-breaking changeset (1 non-breaking changeset(s) seen)"; no D2 registration demanded, no docs/adr/** path touched
node scripts/check-nul-bytes.mjs OK (no raw control bytes)
local eslint over every changed source/test file clean

Changeset level — minor, judged from the sibling precedent (a recorded deviation from the dispatch's "expect major")

The dispatch expected breaking-class = major. Landed as minor, for three reasons: (1) the direct sibling #6970 / PR #7126 — the same acceptance-narrowing class on the same key one layer up — shipped as minor two days ago with the same loud-at-authoring / non-fatal-at-rest stock story; (2) the measured migration surface is zero across both repos, and stored metadata keeps loading (read path runs no Zod validation; write-path validation surfaces as the advisory _diagnostics envelope); (3) the ADR-0087 registration gate, named by the dispatch as the arbiter, demands nothing for this changeset — and a major here would ask changesets to leave the 17.0.0-rc line entirely. If the spec seat or maintainer prefers major-with-registration, saying so reverses this at one-line cost.


Generated by Claude Code

claude added 2 commits August 10, 2026 08:39
…me-token / CEL-envelope (#7127)

Commit 1 of the maintainer's sequenced ruling (2026-08-10): land the
discriminator first, shared by both defaultValue authoring gates, then narrow
FieldSchema.defaultValue on top of it.

New module src/data/default-value-shape.ts:
- discriminateDefaultValueShape / isExpressionEnvelopeDefault — the ENGINE's
  structural predicate (ObjectQL.applyFieldDefaults), verbatim, so the
  authoring gates and the resolver cannot drift apart;
- checkLiteralDefaultValue — the shared literal-vs-stored-contract core
  (valueSchemaFor(def, 'stored'), ADR-0104 D1) both consumers run.

ActionParamSchema's #6970 defaultValue refinement is refactored onto the
shared core with ZERO behavior change — its pins
(action-param-default-value.test.ts) stay green unedited. Action params
deliberately do NOT discriminate: a param default is a pure literal (the
dialog seeds it verbatim; serializeParamValues resolves nothing), and that
stance is now recorded in the shared module's note.

api-surface/data.json + export-origins/data.json regenerated after a real
spec build (dual-snapshot rule).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PiRUoQkTSBBmpyXBY3cVn2
…hape on its own terms (#7127)

Commit 2 of the sequenced ruling: narrow FieldSchema.defaultValue on the
commit-1 discriminator, in the engine's own order — absent (null/undefined;
'' is a REAL default, engine presence semantics) → CEL envelope (structural
acceptance only) → runtime token (per-token × per-type) → literal
(valueSchemaFor(def, 'stored'), the #6970 mechanics one layer down).

Token table as adjudicated on the issue: NOW() legal on datetime/date/time
(both resolvers + types.mdx already support all three); current_user legal on
user + lookup with reference 'sys_user' only (#4560); no token on a
multi-value field (both resolve to one scalar). Near-miss spellings ('now',
'{current_user}') are suggested inside refusals, never accepted as tokens.

The field-value.zod <-> field.zod ESM cycle (dossier §2d) is discharged
structurally: AddressSchema moves to field-value.zod (it IS the enforced
address value contract) and field.zod re-exports it unchanged — the back-edge
becomes type-only (FieldType), erased at runtime. Verified with a real build
plus a dist-eval smoke test, not assumption.

Census re-run against the IMPLEMENTED gate: objectstack 244 + cloud 127 = 371
shipped field-level defaults, 0 refusals (matches the decision dossier
exactly). The two known docs-sample defects (schema.mdx bare CEL source
string, currency object literal) are refusals of this gate and are fixed.

Regenerated after a real spec build: api-surface + export-origins (dual
snapshot — AddressSchema origin move + 4 new exports), docs references,
skill refs, strictness ledger (AddressSchema's strip-site row moved files).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PiRUoQkTSBBmpyXBY3cVn2
@vercel

vercel Bot commented Aug 10, 2026

Copy link
Copy Markdown

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

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
objectstack Ignored Ignored Aug 10, 2026 10:49am

Request Review

…ault-value-discriminator

# Conflicts:
#	content/docs/protocol/objectql/schema.mdx
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

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

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

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

7 release-owned page(s) also reference the affected code. These are read-only:

  • content/docs/releases/implementation-status.mdx (via @objectstack/spec)
  • content/docs/releases/index.mdx (via @objectstack/spec)
  • content/docs/releases/v12.mdx (via @objectstack/spec)
  • content/docs/releases/v13.mdx (via @objectstack/spec)
  • content/docs/releases/v16.mdx (via @objectstack/spec)
  • content/docs/releases/v17.mdx (via @objectstack/spec)
  • content/docs/releases/v9.mdx (via @objectstack/spec)

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

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

@github-actions github-actions Bot added documentation Improvements or additions to documentation protocol:data tests protocol:ui tooling labels Aug 10, 2026
@os-zhuang
os-zhuang marked this pull request as ready for review August 10, 2026 11:25
@os-zhuang
os-zhuang added this pull request to the merge queue Aug 10, 2026
Merged via the queue into main with commit ad6317b Aug 10, 2026
28 checks passed
@os-zhuang
os-zhuang deleted the claude/issue-7127-default-value-discriminator branch August 10, 2026 11:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

2 participants