Skip to content

fix(objectql): a CEL defaultValue stores the declared type's contract shape, not a raw Date (#7373) - #7469

Merged
os-zhuang merged 1 commit into
mainfrom
claude/issue-7373-cel-default-date-normalization
Aug 10, 2026
Merged

fix(objectql): a CEL defaultValue stores the declared type's contract shape, not a raw Date (#7373)#7469
os-zhuang merged 1 commit into
mainfrom
claude/issue-7373-cel-default-date-normalization

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

Closes#7373.

The defect

applyFieldDefaults produces a default three ways, and only two of them honoured the stored-value contract:

branchnormalization
NOW() tokenroutes through resolveNowDefault → the form the declared type stores
literalchecked against valueSchemaFor(def, 'stored') at author time (#7127)
expression envelopeassigned verbatim — no counterpart

The temporal stdlib returns a JS Date (ADR-0053 D1: today() / daysFromNow(n) / daysAgo(n) are UTC-midnight of the reference-tz calendar day; now() the raw instant), so { dialect: 'cel', source: 'daysFromNow(7)' } on a datetime put a Dateobject in the column while valueSchemaFor names an ISO-8601 string. validateRecord accepts a Date on date/datetime by explicit decision, so nothing refused the write — and os migrate value-shapes, which walks stored values against that same schema, reports such a row as a violation by the platform's own scan.

The fix

The expression branch routes a Date result through the same per-type table the NOW() token uses — one table, both branches, not a second copy of the contract:

declared typestored
datetime (and anything else)YYYY-MM-DDTHH:MM:SS.sssZ
dateYYYY-MM-DD
timeHH:MM:SS[.fff]

Measurement behind the choice

In-tree CEL defaultValue census — the dialect: 'cel' corpus is almost entirely expression: (formula fields, virtual — never stored), condition:/when: (rule predicates). Exactly two are defaultValue, one temporal:

sitesourcedeclared typepre-fix stored shape
engine-write-formula-hydration.test.ts:157now()datetimeDate object
engine-select-option-default.test.ts:210'approved'textstring (unaffected)

Per-driver behaviour when handed a Date — measured by reading each driver's write seam:

driverdatetimedatenet effect of this change
driver-sqlcanonicalUtcDatetimetoISOString()toDateOnlyYYYY-MM-DDbyte-identical
driver-mongodbstorageDatetimeValue keeps BSON Date; an ISO string parses to the identical DatestorageDateValueYYYY-MM-DDbyte-identical
driver-memoryimports only coerceTemporalValue (filter comparands); stores writes as handedsameDate object → contract shape

So SQL and MongoDB already stored exactly what the engine now produces; the only stored-shape change is on the memory driver, which is where the defect was visible. This is the split #4597 / #4560 closed for the NOW() token, reappearing on the CEL branch and closed the same way — engine-side, so one answer serves every driver.

Why normalize rather than refuse. Refusing a Date out of a CEL default would make the rule depend on who wrote the value: validateRecord accepts a Date on date/datetime from any caller (if (value instanceof Date) return null), and temporal types are not in ADR-0104's strict value-shape block at all — so there is no strict path that already refuses this on a user write for a defaults path to be bypassing. Refusal would also break the documented envelope (#7244) on precisely the SQL backends where it stores correctly today.

No day-shift. ADR-0053 D1's Date is UTC-midnight of the reference-tz calendar day, and it is read back with toISOString() — UTC getters, the same getUTC* the ADR names for the driver filter path. Reading those parts in local time is the move that would shift a day, and nothing here does it. Pinned directly: at an instant whose UTC day and America/Los_Angeles day differ, a date field defaulted by today() stores the LA day.

Non-Date results pass through untouched (a CEL default's result type is otherwise a runtime concern), as does an Invalid Date — the same totality the driver canons keep.

Pins

packages/objectql/src/engine-cel-default-temporal-shape.test.ts — 8 tests. They assert stored values against valueSchemaForitself rather than a hand-copied shape, and assert the type as well as the text: JSON.stringify renders a Date as its ISO string, which is exactly what made the original defect read as correct.

Subjects: datetime, date, time CEL defaults; the reference-tz day-shift guard.
Controls: NOW() token byte-identical, literal defaults untouched, non-date CEL results passed through, caller-supplied values untouched.

Reverse-verified by restoring the pre-fix out[f.name] = result.value — the 4 subject pins fail, the 4 controls pass in both states.

Verification

  • packages/objectql suite: 177 files / 3135 tests pass
  • Derived gates, all exit 0: check:adr-anchors, check:durability-log-level, check:engine-double-contract, check:stack-collection-maps, scripts/check-engine-split-ratio.mjs, check:nul-bytes
  • check-empty-changeset (1 declaring changeset added), check-changeset-no-major (no major)
  • tsc --noEmit and eslint clean on the changed files

Changeset

patch, on @objectstack/objectql alone. Argued from the gate text: check-changeset-no-major.mjs records that the launch window ships even breaking changes as minor, so major (and the ADR-0087 trio) is out regardless. Between patch and minor, the observable change is confined to correcting values the platform's own value-shape scan already classified as violations, on the one backend that stored them — no API, type, or message-contract change. That matches the recent precedent for contract-correcting behaviour fixes (nested-plugin-view-container-expansion, driver-sql-json-column-operator-refusal, both patch).

No packages/spec touch — deliberately. resolveNowDefault keeps its name so the reference in packages/spec/src/data/default-value-shape.ts stays accurate; only its parameter is renamed nowinstant, since daysFromNow(7) is not "now" and takes the identical per-type treatment.

Refs #7373, #7244, #7127, #4597 / #4560, ADR-0053, ADR-0104.


Generated by Claude Code

…ct shape, not a raw `Date` (#7373)
`applyFieldDefaults` produces a default three ways and only two honoured the
stored-value contract: the `NOW()` token routes through `resolveNowDefault`, a
literal is checked against `valueSchemaFor(def, 'stored')` at author time
(#7127) — and the expression envelope's result was assigned verbatim. The
temporal stdlib returns a JS `Date` (ADR-0053 D1: `today()`/`daysFromNow(n)`/
`daysAgo(n)` are UTC-midnight of the reference-tz calendar day, `now()` the raw
instant), so `{ dialect: 'cel', source: 'daysFromNow(7)' }` on a `datetime` put
a `Date` OBJECT in the column while `valueSchemaFor` names an ISO-8601 STRING.
`validateRecord` accepts a `Date` on `date`/`datetime` by explicit decision, so
nothing refused the write and the divergence was silent — while
`os migrate value-shapes` walks stored values against that same schema and
reports such a row as a violation by the platform's own scan.
The expression branch now routes a `Date` result through the SAME per-type
table the `NOW()` token uses, rather than growing a second copy of the
contract: `datetime` → `YYYY-MM-DDTHH:MM:SS.sssZ`, `date` → `YYYY-MM-DD`,
`time` → `HH:MM:SS[.fff]`.
Storage on SQL and MongoDB is byte-identical to before: `SqlDriver.formatInput`
already coerced a `Date` through `canonicalUtcDatetime`/`toDateOnly`, and
mongodb's `storageDatetimeValue`/`storageDateValue` do the same. What changes is
the memory driver, which applies its temporal canon to filter comparands only
(`coerceTemporalValue`) and stored writes as handed. Same declaration, different
stored shape per datasource — the split #4597 / #4560 closed for the `NOW()`
token, reappearing on the CEL branch and closed the same way, engine-side.
Normalization rather than refusal: refusing a `Date` here would make the rule
depend on WHO wrote the value — `validateRecord` accepts one from any caller,
temporal types are not in ADR-0104's strict value-shape block, and the
documented envelope (#7244) stores correctly on SQL today. Non-`Date` results
pass through untouched, as does an `Invalid Date` (the totality the driver
canons keep). No day can shift: the ADR-0053 `Date` is UTC-midnight OF the
reference-tz day and is read back with UTC getters.
Pins in `engine-cel-default-temporal-shape.test.ts` assert the stored value
against `valueSchemaFor` itself, and assert the TYPE as well as the text —
`JSON.stringify` renders a `Date` as its ISO string, which is what made the
original defect read as correct. Reverse-verified: the four subject pins fail
against the pre-fix assignment; the four controls (`NOW()` token, literals,
non-date CEL results, caller-supplied values) pass in both states.
Refs #7373, #7244, ADR-0053, ADR-0104.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012tjfhdVGuYU9KH7SNbor56
@vercel

vercelBot commented Aug 10, 2026

Copy link
Copy Markdown

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

1 Skipped Deployment
ProjectDeploymentActionsUpdated (UTC)
objectstackIgnoredIgnoredAug 10, 2026 2:39pm

Request Review

@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

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

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

  • content/docs/concepts/metadata-lifecycle.mdx(via @objectstack/objectql)
  • content/docs/data-modeling/formulas.mdx(via packages/objectql)
  • content/docs/deployment/migration-from-objectql.mdx(via @objectstack/objectql)
  • content/docs/deployment/vercel.mdx(via @objectstack/objectql)
  • content/docs/kernel/contracts/data-engine.mdx(via @objectstack/objectql)
  • content/docs/kernel/runtime-services/examples.mdx(via packages/objectql)
  • content/docs/kernel/services-checklist.mdx(via @objectstack/objectql)
  • content/docs/kernel/services.mdx(via @objectstack/objectql)
  • content/docs/permissions/authentication.mdx(via @objectstack/objectql)
  • content/docs/permissions/system-context.mdx(via packages/objectql)
  • content/docs/plugins/index.mdx(via @objectstack/objectql)
  • content/docs/plugins/packages.mdx(via @objectstack/objectql)
  • content/docs/protocol/kernel/index.mdx(via @objectstack/objectql)
  • content/docs/protocol/objectql/query-syntax.mdx(via packages/objectql)
  • content/docs/protocol/objectql/state-machine.mdx(via @objectstack/objectql)

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

  • content/docs/releases/implementation-status.mdx(via @objectstack/objectql)

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.

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

Labels

documentationImprovements or additions to documentationsize/mteststooling

Projects

None yet

2 participants

@os-zhuang@claude