Skip to content

fix(spec): $between endpoints accept the ISO/clock strings the platform produces (#6571) - #7058

Merged
os-zhuang merged 1 commit into
mainfrom
claude/issue-6571-between-string-endpoints
Aug 9, 2026
Merged

fix(spec): $between endpoints accept the ISO/clock strings the platform produces (#6571)#7058
os-zhuang merged 1 commit into
mainfrom
claude/issue-6571-between-string-endpoints

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

Fixes#6571

The sibling half of #5685 (PR #6570). Both of $between's endpoints declared
number | Date | FieldReference — and the platform's own producers put a
string in them. As with the four ordering slots, the declaration did not
merely under-describe reality, it contradicted it; and it did so in the one slot
where a closed interval is the natural spelling of a date window, which is
what makes this the shape an author — an AI author in particular — is most
likely to reach for.

Premise, re-verified against origin/main (not the issue's line numbers)

The issue was filed 2026-08-08 and filter.zod.ts took a docs-prose merge since,
so every claim was re-measured in this worktree at 08863dd18:

claimmeasured
RangeOperatorSchema.$between endpoints lack stringconfirmed, filter.zod.ts:232-238
FieldOperatorsSchema.$between endpoints lack stringconfirmed, filter.zod.ts:422-425
Filter< T > guard is T[K] extends number ǀ Date ? [T[K], T[K]] : neverconfirmed, filter.zod.ts:630
the date-macro walker descends into arraysconfirmed, packages/core/src/utils/filter-tokens.ts:386

Premise valid on all four points.

Evidence the string endpoint is the platform's own output

  1. The date-macro resolver descends into arrays.resolveFilterTokens
    (@objectstack/core, filter-tokens.ts) evaluates the {token} grammar and
    its walk carries an explicit array arm, so a tuple comparand is resolved
    member by member. Every branch of that resolver returns a string — asYmd(…)
    for a calendar day, .toISOString() for the sub-day tokens. So
    { close_date: { $between: ['{current_year_start}', '{current_year_end}'] } }
    becomes { close_date: { $between: ['2026-01-01', '2026-12-31'] } }, whose
    two endpoints were exactly the type this schema declared it refused.
  2. This package's own conformance corpus already spells it — a stronger
    witness than ComparisonOperatorSchema 的 $gt/$gte/$lt/$lte 不含 string,与平台自己只产出字符串的日期宏解析器相矛盾 #5685 had. packages/spec/src/data/temporal-conformance.ts, the
    shared cross-driver expectation table, states three $between cases with
    string endpoints: a datetime range with its {90_days_ago}/{today} token
    twin (:294), the degenerate single-day range (:303), and
    { at: { $between: ['08:00:00', '18:00:00'] } } on a Field.time column
    (:537). A declaration contradicted by the conformance table in the same
    package is not under-describing reality; it is disagreeing with it.
  3. The driver already normalises both ends per column type.
    SqlDriver.coerceFilterValue recurses through arrays member-wise
    (sql-driver.ts:7333, value.map(v => this.coerceFilterValue(table, field, v))),
    and calendarDayBetweenRewrite (:7413) coerces the min and rewrites a
    bare-calendar-day max into the half-open < next-day(max) bound — knex's
    whereBetween being inclusive on both ends, it inherits the same rule $lte
    has (dashboard 的日期区间上界打在 datetime 列上丢失当天数据 —— 默认配置即命中 #3777).

What changed — three spellings, per the ruling

The triage promotion of 2026-08-08 settles the direction, verbatim and
untranslated:

the #5685 precedent settles the fix direction. Fix both spellings plus the Filter guard; #6570's docblock is the template.

  • RangeOperatorSchema — the documentation copy. z.string() joins both
    endpoint unions, plus a docblock on fix(spec): $gt/$gte/$lt/$lte 接受平台自己产出的 ISO 字符串 (#5685) #6570's template and a
    RANGE_ENDPOINT_DESCRIPTION.describe() (module-private, mirroring
    ORDERING_COMPARAND_DESCRIPTION, so it stays off the exported API surface).
  • FieldOperatorsSchema.$between — the enforced copy, the one
    NormalizedFilterSchema validates against and FieldOperators is inferred
    from. ComparisonOperatorSchema 的 $gt/$gte/$lt/$lte 不含 string,与平台自己只产出字符串的日期宏解析器相矛盾 #5685 moved the documentation copy first and had to come back for the
    reachable one; both move together here.
  • Filter< T > — the typed half, mirroring the ordering guard slot for slot.
    T is known here, so it stays type-precise rather than admitting string
    everywhere: a Date field also takes the resolver's ISO strings, a string
    field (a Field.time'08:00:00', an autonumber code) becomes rangeable
    instead of collapsing to never, and a number field stays numbers-only.
    Each endpoint widens independently, so a partially-resolved range
    ([Date, '2026-12-31']) type-checks — which is what a partial macro
    resolution actually hands the author.

Why a bare string and not an ISO refinement

Same finding as #6570, re-measured for the tuple: this schema is
field-agnostic (it never sees which column the range applies to); an ISO
refinement would reject the HH:MM[:SS[.fff]] form field-value.zod.ts's
CLOCK_TIME_TYPES declares NOT Date.parse-able and which the conformance case
above exercises; and date-only vs full-timestamp is already reconciled
downstream by calendarDayBetweenRewrite. Endpoint-vs-column correctness is a
field-TYPED judgement that already has an owner.

Stated plainly in the docblock: ranging over non-temporal text is permitted
but not promised (the order is the backend collation's), and nothing promises
the two endpoints are ordered relative to each other — an inverted [max, min]
range is well-formed and matches nothing, at every backend.

Why the filter-semantics compile faces need zero changes

The five compile faces of the filter-semantics surface (driver-sql, driver-memory,
driver-turso, the analytics normaliser, the formula matcher) are deliberately
untouched, and that is the correct outcome
: this is a declaration-side widening
of a type that no compile face consults. The endpoints were already being
normalised driver-side by column type before ever reaching a predicate —
coerceFilterValue maps arrays member-wise and calendarDayBetweenRewrite owns
the calendar-day max — so every filter that validated before still validates and
every filter that compiled before still compiles the same SQL. Re-measured in
this worktree; the sentence still holds.

Scope was held to $between exactly. #6520 ($icontains) is a different
operator and is explicitly out of this card.

Tests, both directions

packages/spec/src/data/filter.test.ts:

  • now accepted: the two calendar-day strings a token range resolves to; a
    range of full ISO instants; the ['08:00:00', '18:00:00'] wall-clock range
    the conformance corpus pins; mixed endpoints (Date + string, string +
    { $field }) proving each union resolves on its own.
  • still accepted (additive): numbers, Dates, { $field } references.
  • still rejected: an endpoint rangeable at no backend (true, a bare
    object), and non-two-element arity.
  • enforced copy pinned: the same shapes through FieldOperatorsSchema and
    NormalizedFilterSchema, so the two spellings cannot drift.
  • typed half: a Filter< Deal > block checked by tsc, not vitest.

The rejection cases assert through safeParse and check the failing issue's
path (['$between', 0] vs ['$between', 1]) rather than a bare toThrow():
a tuple carries two independent unions, and toThrow() cannot tell a min-side
refusal from a max-side one. The ADR-0112 code/status envelope does not
apply — these are Zod parse verdicts on a declaration surface, not runtime
refusals.

Reverse verification (direction predicted first)

Predicted before running: reverting the union widening turns the new
string-endpoint acceptance pins RED, and the Filter< T > block goes red under
tsc only — vitest never type-checks, so the typed pin is green under vitest on
the reverted schema by construction. Result recorded in the dev report.

Gates

node scripts/check-adr-0087-registration.mjs --base origin/main
this PR adds no declared-breaking changeset (additive widening; no D2/D3
registration owed). pnpm --filter @objectstack/spec check:generated → all 10
generated artifacts up to date, nothing regenerated and no generated tree
touched (the operator .describe() strings do not reach
content/docs/references/, same as #6570's). scripts/check-nul-bytes.mjs OK.

Changeset graded minor on @objectstack/spec, matching #6570's own changeset
for the same class of widening.


Generated by Claude Code

…rm produces (#6571)
The sibling half of #5685. Both of `$between`'s endpoints declared
`number | Date | FieldReference`, while the platform's own producers put a
string in them:
- `resolveFilterTokens` (`@objectstack/core`) walks INTO arrays, so a token
range resolves member by member and every resolver branch returns a string:
`['{current_year_start}', '{current_year_end}']` -> `['2026-01-01', '2026-12-31']`.
- This package's own `temporal-conformance.ts` corpus states three `$between`
cases with string endpoints, including `['08:00:00', '18:00:00']` on a
`Field.time` column.
- `SqlDriver.coerceFilterValue` already normalises arrays member-wise, and
`calendarDayBetweenRewrite` coerces the min and rewrites a bare-calendar-day
max into the half-open next-day bound (#3777).
Widened in all three places the contract is spelled — `RangeOperatorSchema`
(documentation), `FieldOperatorsSchema` (the enforced copy behind
`NormalizedFilterSchema`), and the `Filter<T>` helper, where `T` is known so the
guard stays type-precise and each endpoint widens independently.
Declaration-side and additive: no producer, caller or driver changed.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PiRUoQkTSBBmpyXBY3cVn2
@vercel

vercelBot commented Aug 9, 2026

Copy link
Copy Markdown

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

1 Skipped Deployment
ProjectDeploymentActionsUpdated (UTC)
objectstackIgnoredIgnoredAug 9, 2026 12:32pm

Request Review

@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.

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

Labels

documentationImprovements or additions to documentationprotocol:datasize/mteststooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[finding][spec] $between 的两个端点也不含 string —— 与 #5685 同一处矛盾,而日期宏解析器会走进数组

2 participants

@os-zhuang@claude