fix(spec,drivers): a datetime $lte or $between maximum on 9999-12-31 includes the whole last supported day (#20600) - #20643
Conversation
…r (red on base) Claude-Session: https://claude.ai/code/session_014EJ1ED8X4MMrT18BhVx4tx Co-authored-by: Claude <noreply@anthropic.com>
… not the five-digit '10000-01-01' Claude-Session: https://claude.ai/code/session_014EJ1ED8X4MMrT18BhVx4tx Co-authored-by: Claude <noreply@anthropic.com>
…nd for a whole-day bound on 9999-12-31 Every reader of nextUtcCalendarDay handles UNBOUNDED_ABOVE: a lone $lte asks only for a value, a $between or dateRange keeps its minimum. The shared temporal conformance kit carries the last-day cases to every backend. Claude-Session: https://claude.ai/code/session_014EJ1ED8X4MMrT18BhVx4tx Co-authored-by: Claude <noreply@anthropic.com>
…ompiles its bound Claude-Session: https://claude.ai/code/session_014EJ1ED8X4MMrT18BhVx4tx Co-authored-by: Claude <noreply@anthropic.com>
…ay, never UNBOUNDED_ABOVE Claude-Session: https://claude.ai/code/session_014EJ1ED8X4MMrT18BhVx4tx Co-authored-by: Claude <noreply@anthropic.com>
… and the mongo translator's last-day pins Claude-Session: https://claude.ai/code/session_014EJ1ED8X4MMrT18BhVx4tx Co-authored-by: Claude <noreply@anthropic.com>
…st-day-unbounded-above
… as a day, never UNBOUNDED_ABOVE Claude-Session: https://claude.ai/code/session_014EJ1ED8X4MMrT18BhVx4tx Co-authored-by: Claude <noreply@anthropic.com>
…ABOVE Claude-Session: https://claude.ai/code/session_014EJ1ED8X4MMrT18BhVx4tx Co-authored-by: Claude <noreply@anthropic.com>
changeset) Claude-Session: https://claude.ai/code/session_014EJ1ED8X4MMrT18BhVx4tx Co-authored-by: Claude <noreply@anthropic.com>
…st-day-unbounded-above
…erasing the query options Claude-Session: https://claude.ai/code/session_014EJ1ED8X4MMrT18BhVx4tx Co-authored-by: Claude <noreply@anthropic.com>
…st-day-unbounded-above
…st-day-unbounded-above
📓 Docs Drift CheckThis PR changes 9 package(s): 10 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 6 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 145 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin d1745f2d3aa234ed37e0636425141f278bd49566 && git checkout d1745f2d3aa234ed37e0636425141f278bd49566
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin f4ce10c89dcb87b496def16aacee9107155d5d57 49a2585988b92e62dead92c7bd1ac2894009e59b && git checkout -B drift-repro f4ce10c89dcb87b496def16aacee9107155d5d57 && git merge --no-ff 49a2585988b92e62dead92c7bd1ac2894009e59b
node scripts/docs-audit/affected-docs.mjs --json f4ce10c89dcb87b496def16aacee9107155d5d57
|
…ondition | undefined Claude-Session: https://claude.ai/code/session_014EJ1ED8X4MMrT18BhVx4tx Co-authored-by: Claude <noreply@anthropic.com>
Contract reviewServed-tier: Inputs: card #20600 (body; triage ① Derived judgmentsThe helper (right at run time, wrong at the type level as shipped). Reader census (my own, on Per operator, each named right: a lone Hypothesis 3 (the dev's partial falsification): true. Every call site above sits in an Conformance: right. Row Out-of-scope finding 1: confirmed from the code. Gate coverage (the check-runs on this head, read once): 35 runs — 30 success, 3 skipped, 2 failure. Green: ② Semver levelLevels per package: right. The Clause-② open question: B, as an amendment, not a re-arming. Judged on the repo's own rules: (1) ③ Boundary flags
FAIL reasons: (1) Implemented-by: VERDICT: FAIL Generated by Claude Code |
…st-day-unbounded-above
…hared guard, one type across the ./data declaration files A unique symbol is nominal to each declaration file, and @objectstack/spec ships ./data as index.d.mts and index.d.ts; a program meeting both saw two unrelated types (TS2367 / TS2339 in @objectstack/dogfood). The sentinel's type is now symbol & a structural brand, and every reader narrows with isUnboundedAbove. Claude-Session: https://claude.ai/code/session_014EJ1ED8X4MMrT18BhVx4tx Co-authored-by: Claude <noreply@anthropic.com>
…UtcCalendarDay, with its ADR-0087 disposition (seat ruling B) Claude-Session: https://claude.ai/code/session_014EJ1ED8X4MMrT18BhVx4tx Co-authored-by: Claude <noreply@anthropic.com>
…bove and isUnboundedAbove Claude-Session: https://claude.ai/code/session_014EJ1ED8X4MMrT18BhVx4tx Co-authored-by: Claude <noreply@anthropic.com>
Contract reviewServed-tier: Inputs: card #20600 (body; triage ① Derived judgmentsRound-1 reason 1 (two nominal identities): closed, from the code and from CI. The sentinel's type is The ruling's ⛔ holds: the sentinel is not hidden inside Runtime value and guard: right. Still Reader census (my own
Per operator, each right. A lone Conformance kit: right. Row Public surface, each named. (1) The merge of The dev's new noted item (other exported Gate coverage (42 check-runs on the head, read once): 36 success, 5 skipped, 1 in progress, 0 failure. Type identity → ② Semver levelLevels per package: right. Clause-②: yes (widening) — right, well-formed, carried in both places (PR body line 3 as patched by the seat; changeset line 15), naming the three exports on spec Round-1 reason 2 (ruling B): closed. The changeset carries ③ Boundary flags
Implemented-by: VERDICT: PASS Generated by Claude Code |
… authoring and at construction (objectstack-ai#20586) (objectstack-ai#20669) Fixes objectstack-ai#20586 Clause-②: yes (narrowing) The `Clause-②` line above is the claim's (comment 5891827128), copied as it stands. The changeset carries the same value. Session `session_01DEvba2nBuD4tWzfq8r8NFY` (PM dispatch, `domain:engine` seat 1, mode:subagent), branch `claude/issue-20586-forced-local-refuses-sync-url`. The container restarted mid-run. The branch was fast-forwarded to `origin/main` `f4ce10c89` (BASE) before the first edit, and later got a true merge of `origin/main` at `6bff748bb`. **Every final reading below was taken at head `cfe05ec40`** unless it says otherwise. ## What changes A turso config that forces `mode: 'local'` on a `file:` url (or `:memory:`) beside a non-empty `syncUrl` is now refused at both doors, with one message. Triage 5884612522 directed this: "A forced `mode: 'local'` beside `syncUrl` (or `sync`) is refused at both schema copies and at the constructor, naming the conflict." - **Authoring.** `tursoTransportIssues` in `packages/spec/src/data/driver/turso.zod.ts` gains a forced-local arm, placed after objectstack-ai#20437's forced-replica arm. It returns one `custom` issue on **`mode`**, the key the runtime would ignore, as objectstack-ai#20437's does. It reaches `DatasourceSchema` (as `config.mode`), `validateDriverConfig`, `defineStack` / `os validate`, and a save or test connection through the datasource admin service. - **Construction.** `new TursoDriver()` refuses the same config with `VALIDATION_ERROR` / 400, before `super()`, as the last of the sync-key refusals. The message is a module constant, `LOCAL_MODE_WITH_SYNC_URL_REFUSAL`, next to objectstack-ai#20437's `REPLICA_MODE_WITHOUT_SYNC_URL_REFUSAL`. It is thrown through the same `refuseIgnoredSyncKey` helper, and the parity table pins it byte-equal to the schema's issue. - **The driver mirror** (`packages/drivers/driver-turso/src/spec/turso.zod.ts`) carries the same arm byte for byte. The mirror declares no `mode` and strips an authored one, so the arm is unreachable through it. It stays for copy parity, like the mirror's other forced-mode branches (see Deviations). - **"(or `sync`)".** `sync` with no `syncUrl` was already refused in every mode (objectstack-ai#20200). `sync` beside a `syncUrl` under a forced local mode is refused by this new arm. So every `sync` shape under a forced local mode is covered, with no extra arm. The message, the same text at both doors: > `mode: 'local'` makes this datasource a plain local database, but `syncUrl` names a remote to replicate from: the database would still be synced with that remote as an embedded replica, so the declared local mode would be ignored — the turso driver refuses this configuration when it starts. For an embedded replica, drop `mode` and keep `syncUrl` beside the local file: `url: 'file:./data/replica.db'`. For a plain local database, drop `syncUrl` (and `sync`). It names both ways out. It echoes no url and no `syncUrl`, and carries no tracker id. ## H1: the premise, measured before the change (held) At BASE `f4ce10c89`, a temporary probe ran on the driver source (deleted after one run, never committed). The config was a `file:` url, `syncUrl`, `sync: { intervalSeconds: 1 }`, and a client that counts `sync()` calls: | config | transportMode | syncs after connect | isSyncEnabled() | interval | syncs after 1.3 s | | --- | --- | --- | --- | --- | --- | | forced `mode: 'local'` + `syncUrl` | `local` | 1 | true | started | 2 | | `syncUrl`, no `mode` (the replica control) | `replica` | 1 | true | started | 2 | | forced `mode: 'local'`, no `syncUrl` | `local` | 0 | false | none | 0 | So a forced local mode beside `syncUrl` ran exactly as a replica, and only the label said `local`. The pins holding today's answer passed at BASE: - the parity row "file: + syncUrl under a forced mode: 'local'" read constructor accept and spec accept (parity file 177 passed / 22 skipped); - the spec test's accept fixture `{ url: 'file:./data/app.db', mode: 'local', syncUrl: … }`; - the objectstack-ai#20200 file's control "sync beside syncUrl under a forced mode: 'local' stays accepted". `isSyncEnabled()` is `!!this.tursoConfig.syncUrl && this.libsqlClient !== null`, as H1 states. ## H2: objectstack-ai#20437 is the template (held), and what differs in this direction It is mirrored arm for arm: one spec arm on `mode`, a byte-identical mirror arm, a module constant, one constructor check, a new refusal test file, a parity flip and a new D3 entry. What differs: - The ignored key is `mode`, as in objectstack-ai#20437, but the cause is the opposite. A `syncUrl` is present, so the conflict is between two declared keys, not a missing one. The message therefore offers "drop `mode`" (replica) or "drop `syncUrl` (and `sync`)" (local). - The arm is reachable on an in-memory url too. A local mode accepts `:memory:` (`localEngineDefect` passes it), so `:memory:` and `file::memory:` beside `syncUrl` under a forced local mode meet this refusal. The replica way out points at a file url, so it is correct there as well. - No config meets two issues here. objectstack-ai#20437 has one row where the schema raises `sync` and `mode` together. This arm needs a non-empty `syncUrl` and the `sync` refusal needs none, so they are exclusive, and every row here raises exactly one issue. ## H3: ADR-0087 disposition — a new D3 entry, `registered` Neither existing turso entry's `surface` names this shape. `turso-config-transport-mismatch-refused` lists the url, in-memory, WebSocket and remote-`syncUrl` combinations. `turso-config-forced-replica-without-sync-url-refused` is the opposite shape. The gate accepts `registered` only with an id that is new in this diff, and `already-registered` only for an id that already covers the refusal. So a new entry lands, following objectstack-ai#20437's precedent: `packages/spec/src/migrations/entries/semantic/18.turso-config-forced-local-with-sync-url-refused.ts`, id `turso-config-forced-local-with-sync-url-refused`. - `src/migrations/registry.ts` was regenerated by `gen:migration-registry`, never by hand: +50 / -0, one entry. It reads `320 semantic, 236 retired-key, 207 retired-def`. - The changeset carries the disposition marker `registered turso-config-forced-local-with-sync-url-refused`. - `check-adr-0087-registration` reads it as `[BREAKING+bang+clause-②-narrowing] registered turso-config-forced-local-with-sync-url-refused (new here: …)`. - `check:generated` reads "All 15 generated artifacts are up to date", including `check:spec-changes` and `check:upgrade-guide`. ## H4: refusal order (held; the new arm is last) - **Spec.** The arm sits after objectstack-ai#20437's forced-replica arm, inside the non-remote branch. Every url-shape refusal returns first, and they are unchanged. - **Constructor.** The check sits after the forced-replica check, which is last among the existing refusals. - **Exclusivity.** Every other refusal is exclusive of this one except the url refusals, which both doors raise first. The remote refusals need remote mode. The `sync` refusal needs no `syncUrl`. The replica refusal needs a replica mode. - **Pins.** ORDER rows cover a remote url, a bare path, `sync` with no `syncUrl`, and objectstack-ai#20437's forced replica with no `syncUrl`. Each keeps its own message. ## H5: producer census (before any edit, at BASE `f4ce10c89`, repo `objectstack-ai/objectstack`) | query | hits | | --- | --- | | `git grep -E "mode:\s*['\"]local['\"]\|\"mode\"\s*:\s*\"local\""` (whole tree) | 49 in 11 files: 11 in two CHANGELOGs, 38 in `packages/drivers/driver-turso` and `packages/spec` (source, tests, README). Of those, 3 author the shape beside a `syncUrl`, all tests: the parity row, the spec accept fixture and the objectstack-ai#20200 control, each flipped here. The other `mode: 'local'` spellings carry no `syncUrl` or are describe labels | | `syncUrl` / `sync_url` / `SYNC_URL`, case-insensitive, per tree (turso control count in brackets) | `examples/` 0 [2], `packages/create-objectstack` 0 [0], `skills/` 0 [6], hand-written `content/docs` 0 [109], `apps/` 0 [0] | | env names read in `packages/**/src` (non-test) matching `TURSO_*` / `OS_DATABASE_*` / `OS_TURSO_*` | `OS_DATABASE_URL`, `OS_DATABASE_DRIVER`, `OS_DATABASE_AUTH_TOKEN`, `OS_DATABASE_POOL_MAX`, `OS_DATABASE_SQLITE_JOURNAL_MODE`, `TURSO_DATABASE_URL`, `TURSO_AUTH_TOKEN`, `TURSO_TOKEN` (plus code constants). None maps to `mode` or `syncUrl` | | who sets a turso `mode` | only `buildTursoDriverConfig`'s `mode` reader (`packages/services/service-datasource/src/turso-driver-config.ts:205`), from an authored `datasource.config.mode`. Its two callers are `packages/runtime/src/turso-driver-factory.ts:288` and `packages/services/service-datasource/src/default-datasource-driver-factory.ts:1337`. No other non-test `new TursoDriver` / `createTursoDriver` call sets `mode` | No shipped in-repo producer authors the shape, and no deployment default sets it, so the `needs_decision` branch does not trigger. **`objectstack-ai/cloud`: NOT MEASURED.** Attaching it read-only was refused by the session's permission classifier. The seat or the maintainer should census cloud before this lands. ## Tests (at `7ee1acb58`, the last code commit; the merge after it touched no turso or spec path) | suite | result | | --- | --- | | `@objectstack/driver-turso` vitest, whole package | 80 files · 2175 passed · 33 skipped · exit 0 | | `@objectstack/driver-turso` typecheck (`tsc --noEmit`) | exit 0. `tsc --listFilesOnly` lists all 3 touched test files | | `@objectstack/spec` vitest `--project local`, 3 shards | 575 files · 16946 passed · 1 todo (6106 + 5231 + 5609), exit 0 each | | `@objectstack/spec` typecheck (tsc + scripts + `check:test-typecheck`) | exit 0 | | `@objectstack/spec` `check:generated` (after the spec build) | "All 15 generated artifacts are up to date" | The 33 skips are the parity table's forced-mode rows for the mirror, which strips `mode`. There were 22 before; this PR adds 11: 8 `mode` rows, 2 ORDER `url` rows and 2 accept controls, less the 1 row that moved. - **`spec/turso-config-constructor-parity.test.ts`.** The row "file: + syncUrl under a forced mode: 'local'" flips from `accept` to `refuse`, `refusedOn: 'mode'`. Seven more `mode` rows are added: no `sync`, an uppercase `FILE:` url, a url behind whitespace, `timeoutMs`, a `wss://` `syncUrl`, `:memory:` and `file::memory:`. Two ORDER rows keep their `url` refusal (a `libsql://` url and a bare path, each beside `syncUrl` under a forced local mode). Two accept controls are added: a forced local mode alone, and one beside an empty `syncUrl`. `SYNC_KEY_REFUSALS` takes the new `mode` rows, so each of the 8 asserts the constructor's message equals the spec issue's. New floors: `mode` at least 12, the forced-local `mode` rows at least 8, sync-key refusals at least 20. The unforced `file:` + `syncUrl` replica row ("a replica: file: + syncUrl") is the unchanged control triage names. - **`turso-driver-ignored-sync-key-refusal.test.ts`.** The control "sync beside syncUrl under a forced mode: 'local' stays accepted" is removed, because it pinned the defect. The header points to the new file. The objectstack-ai#20200 refusal assertions are untouched. - **`turso-driver-forced-local-with-sync-url-refusal.test.ts` (new).** The refusal is asserted as the envelope (`code` + `status`), plus its first sentence and both ways out. It covers `file:` and `FILE:` urls, `:memory:`, `file::memory:`, and a config beside `sync`, `timeout` or `encryptionKey`. With a supplied client it is refused before any client work. It also covers `createTursoDriver()` and a check that neither url is echoed. ORDER: a remote url, a bare path, `sync` with no `syncUrl`, and a forced replica with no `syncUrl` each keep their own refusal. CONTROLS: the unforced replica still connects and syncs once; a forced local mode with no `syncUrl` or an empty one syncs nothing; a forced replica beside `syncUrl` and `:memory:` under a forced local mode both construct; and `detectMode` still answers `local`. - **`packages/spec/src/data/driver/turso.test.ts`.** The accept fixture becomes `{ …, mode: 'local', syncUrl: '' }`, because an empty `syncUrl` is unset. A new block asserts the refusal on `mode` over 6 configs, the url echo, url-refusal ORDER, both authoring doors (`config.mode` and `validateDriverConfig`), and the controls (the unforced replica with and without `sync`, and a forced local mode alone, beside an empty `syncUrl`, and on `:memory:`). **Reverse verification** ran through `scripts/ablation-replace.mjs` from the committed state at `7ee1acb58`. Each direction was predicted before the run, and all three matched: 1. **The constructor refusal disabled.** ` if (mode === 'local' && config.syncUrl) {` became `if (false && …) {`, and the mutation landed (anchor 1 → 0, blob `f9af3e93c573` → `35630f956f8f`). Predicted 26 RED. Got **26 failed** / 201 passed: the new file's 10 refusal cases, plus the parity table's 8 constructor verdicts and 8 byte-equality pins. No ORDER or CONTROLS case failed. Restored: blob == HEAD, `git diff HEAD` empty. 2. **One byte of the driver copy.** A doubled space was put after the constant's first sentence (blob → `019694bf2026`). Predicted exactly the 8 byte-equality pins. Got **8 failed**, all "the constructor's message is the spec contract's, byte for byte", while every verdict and first-sentence case stayed green. Restored the same way. 3. **The spec arm disabled** in `packages/spec/src/data/driver/turso.zod.ts` (blob `eed1efdaa33a` → `7850f9fcb53e`), against the spec's own source-level test. Predicted 3 RED. Got **3 failed** / 36 passed: the refusal, the url-echo and the both-doors cases. ORDER and controls stayed green. Restored the same way. The driver tests import the driver from `src`, so ablations 1 and 2 needed no build. Ablation 3 was read on the spec's own source tests only. The parity table's spec half reads the built spec dist, and that half was not re-ablated. ## Gates `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` ran at head `cfe05ec40`, after the final commit and the merge. It lists 10 paths vs merge base `6bff748bb` and **91 commands**. Every command ran, with its exit code written to disk before any pipe. `--ran` reconciliation reads `91 derived famil(ies) accounted for — 89 run, 2 NOT-MEASURED (2 DERIVED from a recorded exit 3)`, with 0 UNRUN. - **NOT MEASURED (exit 3, PREREQUISITE NOT MET):** `check:dual-build-cjs-loads` (workspace packages with no `dist/`) and `check:type-check-debt` (it needs a whole-workspace build). Both are CI's run. - **Run twice:** `check:doc-formula-expressions` and `check:lean-entry-closure` first answered exit 3. They are exit 0 after building the `@objectstack/lint` and `@objectstack/objectql` closures. - **Notable readings:** - `check-adr-0087-registration`: `registered turso-config-forced-local-with-sync-url-refused` (new here), BREAKING, bang, clause-② narrowing; - `check-changeset-no-major`: "This diff introduces no `major` bump"; - `check-empty-changeset`: exit 0; - `check:migration-registry`, `check:spec-changes`, `check:upgrade-guide`, `check:api-surface`, `check:authorable-surface`, `check:docs`, `check:doc-authoring`, `check:nul-bytes`, `check:test-source-alias`, `check:cross-package-test-inputs` and `check:driver-conformance`: exit 0. - **Narrowed lint:** `eslint --no-inline-config --format json` over the 9 changed TS files reports 9 files, 0 errors and 0 warnings. The population is `eslint.config.mjs`'s lint object, `files: ['**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}']`; the changeset `.md` is outside it. Invariance holds because the config enables no type-aware linting (its one `parserOptions.project` mention is a comment saying so), so this diff cannot move any untouched file's verdict. The full `pnpm lint` is CI's. - **Not run locally, left to CI:** the whole-workspace type-check lanes; the Test Core, Dogfood and Build Core jobs; and the downstream suites of `@objectstack/service-datasource`, `@objectstack/runtime` and `@objectstack/cli`. The narrowing is declared. The public surface's bytes are unchanged (`check:api-surface` and `check:authorable-surface` are green, and refinements are not in the JSON Schema). The census above finds no consumer fixture that authors the refused shape. **Driver-conformance ledger:** `check:driver-conformance` read `OK — 50 covered cell(s), 0 in the DEBT ledger, 0 exempt` both before (`f4ce10c89`) and after (`cfe05ec40`). `driver-turso` is `ok` on all 10 case-sets both times, so there was no movement. ## Deviations (declared) - **"Both schema copies" is met as text, not as a verdict, at the mirror.** Triage's pin names the refusal "at both schema copies". The driver mirror declares no `mode` key and strips an authored one, so it cannot see a forced mode. It carries the arm byte for byte and still accepts this config, judging it as the replica its url and `syncUrl` select. That is objectstack-ai#20437's documented reading, and the parity table skips the mirror half of forced-mode rows. Giving the mirror a `mode` key is outside this card: the parity test's header calls that shortness "not this card's to change". The D3 entry's `surface` and the changeset say this rather than claiming the mirror refuses. - **Cloud producers were not measured** (H5). Attaching `objectstack-ai/cloud` was refused by the permission classifier. Nothing in-repo triggers `needs_decision`. - **The H1 runtime probe used a supplied client.** A client that counts syncs stood in for `@libsql/client`, so the probe made no network call. The arm it exercises (`connect()`'s `syncUrl` branch) is the one a driver-built client takes too. ## Acceptance notes - `packages/drivers/driver-turso/README.md`: the constructor-refusal list now reads four sync settings and gains an item for a forced `mode: 'local'` beside a non-empty `syncUrl`, with both ways out. Patch round 1 (`a0c4c033a`, README text only) made this change after contract review 5894260625 found the "three sync settings" sentence false at `cfe05ec40`. Its gate re-run reconciles 91 derived, 89 run, 2 NOT-MEASURED, 0 UNRUN. - The spec's `mode` key keeps its one-line TSDoc. The rule is carried by the refusal text and by `TursoDriverConfig.mode`'s TSDoc in the driver, which now names it (as does `syncUrl`'s). - The existing D3 entries `turso-config-transport-mismatch-refused` and `turso-config-forced-replica-without-sync-url-refused` are untouched. Both stay true. - Not touched: `turso-driver.ts`'s remote filter lowering (`toRemoteUpperBound`, the `$between` / `$lte` arms), which the spec lane's PR objectstack-ai#20643 edits. This PR's hunks there are the file header, the `syncUrl` / `mode` TSDoc, the new constant beside the sync-key refusals, and one constructor check. --- _Generated by [Claude Code](https://claude.ai/code/session_01DEvba2nBuD4tWzfq8r8NFY)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
Fixes #20600
Clause-②: yes (widening) — three new exports on
@objectstack/spec(data) and@objectstack/core: the constantUNBOUNDED_ABOVE, its typeUnboundedAboveand the guardisUnboundedAbove;nextUtcCalendarDayanswers the constant for9999-12-31, the one input that used to answer the five-digit'10000-01-01'; a$lte/$betweenmaximum on that day that answered no rows on SQLite now answers the whole day. Nothing any door accepted before is refused, and nothing is removed or renamed.BREAKING for TypeScript and JavaScript callers of
nextUtcCalendarDay(seat ruling B,5892121354): its return type gains a member and its answer for one input changes from a string to a symbol, landing asminorunder the launch-window convention; the changeset carries the banner and the ADR-0087 dispositionnot-required (no-migration-prescription).Direction from triage
5886142901(binding, not re-opened): neithernullnor a five-digit year. Every supported value is at most the last millisecond of9999-12-31, so the whole-day bound past the last day is "unbounded above": the helper answers that explicitly, as a distinct value, and the drivers compile no upper bound for it.The change
packages/spec/src/data/calendar-day.ts: new exportsUNBOUNDED_ABOVE, its typeUnboundedAbove(asymbolwith a structural brand) and the guardisUnboundedAbove.nextUtcCalendarDayreturnsstring | UnboundedAbove | nulland answersUNBOUNDED_ABOVEfor the one real day whose successor has noYYYY-MM-DDspelling. The type is structural because./dataships asindex.d.mtsandindex.d.ts: aunique symbolwas two unrelated types in a program meeting both (the redType Check · workspace), while two copies of the branded alias are one type. It stays a symbol, so a template literal, a relational comparison and astringtarget still refuse it; readers narrow withisUnboundedAbove(===compares but does not narrow).@objectstack/corere-exports all three.$lte(or its AST spelling): no bound, only "the value is not null" (IS NOT NULLon SQL and the analytics echo,$ne: nullon memory and mongo, a value check in the engine'shaving);$between/betweenmaximum, an explicit analyticsdateRangeend: the range keeps its minimum alone;formula's RLScheck, the draft preview): every value that denotes an instant is inside the bound, and any other value is compared as written.$gte,$gt,$ltand$eqon9999-12-31do not read the helper and are unchanged (midnight-anchored;$eqis that midnight instant).9999-12-30and every earlier day compile the same bound as before.packages/spec/src/data/temporal-conformance.ts) gains the rowz_last(9999-12-31T10:00:00.000Z,nativewriter form) and five last-day cases ($lteondatetimeand ondate, two$betweenmaxima with their analyticsdateRangespellings, and the9999-12-30control), so every backend the kit drives, theTemporal Conformance (live PG + MySQL)job included, is held to this answer. Three existing$gte/$gtcases now also expectz_last.Reader census (the answer of
nextUtcCalendarDay, measured withgit grepatc1d8051e0a)driver-sqlcalendarDayExclusiveUpperBound(+calendarDayUpperBoundRewrite,calendarDayBetweenRewrite, thewhereemitter)sql-driver.ts10000-01-01…; SQLite: no rowsIS NOT NULL/ the minimum onlydriver-tursoremote lowering (inherits the rewrite)turso-driver.tstoRemoteUpperBound$null: false/$gteonlydriver-memory$lte,$betweenand their AST spellingsmemory-driver.ts(4)$lt '10000-01-01': no rows$ne: null/$gteonlydriver-memorycubelte(mingo and SQL echo),dateRangewindowmemory-analytics.ts(3)$ne: null/IS NOT NULL/ start onlydriver-mongodb$lte,$betweenmongodb-filter.ts(2)$ltthe stored form of10000-01-01$ne: null/$gteonlyformulalteBound(RLScheck)matches-filter.ts'2026-…'value sorts above'10000-01-01': write deniedobjectqlwholeDayUpperBound(having, per-aggregationfilter)having-filter.tsminonlyservice-analyticspreviewlteBound,dateRangepreview-evaluator.ts(2)service-analyticsnative SQLlte,dateRangenative-sql-strategy.ts(2)IS NOT NULL/ start onlyservice-analyticsObjectQL echodateRangeobjectql-strategy.ts10000-01-01specutcInstantMsvalidity testcalendar-day.ts!== nullcoreanalytics-date-range.test.ts,specdate-range-presets.test.tsThe seat's list also named
coretemporal-storage-form.ts/analytics-date-range.ts,specdate-range-presets.ts/temporal-conformance.tsanddriver-mongodbmongodb-temporal.ts: they mention the helper in comments only and read no answer. The claimedservice-analyticsfilesanalytics-service.ts,cube-registry.ts,dataset-executor.ts,dataset-compiler.tsdo not read it and are untouched.Shape, measured (hypothesis 2)
Round 1 kept
unique symboland measured 3 of 16 reader sites refused and 13 silent under the widened type. The at-tier review found the type nominal per declaration file:Type Check · workspacewent red in@objectstack/dogfood(TS2367 atturso-driver.ts2640, TS2339 at 2641), reproduced locally ate197faa978with CI's own command. Round 2 types the sentinelsymbolplus a structural brand and adds the shared guard: the same workspace typecheck is green at49a2585988(135 of 135 tasks, dogfood included), and a scratch consumer compiled against the builtdist/data/index.d.tsandindex.d.mtstogether compares and narrows across the two files while a template literal, a relational comparison and astringtarget still refuse the member (TS2731, TS2469, TS2322). A string sentinel is excluded by ruling; a tagged object compiles silently in a template literal.Verification
Premise, on the base
c1d8051e0a, throughPOST /api/v1/data/:object/query(the newpackages/rest/src/data-query-calendar-day-last-day.test.ts, process in America/New_York, a private PostgreSQL 16.13 with server zone Asia/Shanghai):where opened_at$lte '9999-12-31'c26,prev,open,mid,last$between ['2026-01-01', '9999-12-31']$between ['9999-12-31', '9999-12-31']open,mid,lastopen,mid,last$notof$lte '9999-12-31'noneincludednonenone$lte '9999-12-30'(control)c26,prevc26,prev$gte/$gt/$lt/$eq'9999-12-31'Rows:
c262026-07-15T14:00Z(the card's control),prev9999-12-30T10:00Z,open9999-12-31T00:00Z,mid9999-12-31T10:00Z(the card's row),last9999-12-31T23:59:59.999Z,noneno value.Reverse verification (fix committed first at
f4d15dd506, mutation throughscripts/ablation-replace.mjsunder atraprestore to theHEADblob): the helper's last line was replaced byreturn next || 'ABLATION_20600';, so it answers the successor spelling again (the five-digit'10000-01-01'for the last day; the marker never fires and only proves the mutation reacheddist); anchor 1 to 0 and replacement 0 to 1 on disk;pnpm --filter @objectstack/spec build;scripts/ablation-dist-preflight.mjsfound the mutation indist. Mutated leg:speccalendar-day 2 failed;driver-sql9 failed (the SQLite and legacy-storage cells; the PostgreSQL cells stayed green, as on the base);driver-memory11;driver-mongodb3;formula10;objectql3;service-analytics17;driver-turso12;rest1 (the SQLite cell). Restore: blob equalsHEAD,git diff HEADempty, spec rebuilt, the preflight found the mutation in no built file, and the same battery went green (16, 135 plus 1 MySQL skip, 83, 50, 84, 89, 122, 150, 4).Tests and gates at
49a2585988(origin/mainf4ce10c89dmerged): the whole-workspace typecheck of theType Check · workspacejob, 135 of 135 tasks; the reader battery under America/New_York on SQLite (spec 28, core 73, driver-sql 91, driver-memory 83, driver-mongodb 84, formula 84, objectql 89, service-analytics 122, driver-turso 150, driver-sqlite-wasm 71, rest 4) all green;check:generatedall 15 up to date after regeneratingapi-surfaceandexport-origins; 94 derived gates all exit 0 and reconciled with--ran(94 run, 0 NOT MEASURED); narrowed lint 23 files, 0 errors, 0 warnings. The round-1 full suites (atf7a61d512eande197faa978) and the live PostgreSQL cells stand from round 1; round 2 changes the readers' test from=== UNBOUNDED_ABOVEtoisUnboundedAbove(…), whose body is that comparison.Acceptance notes
$eq '9999-12-31'to "still mean that whole day". No reader applies the whole-day reading to$eq: on every backend a bare day under$eqon adatetimeis that day's midnight instant, and none of them readsnextUtcCalendarDayfor it. It is unchanged here and pinned as such ($eq '9999-12-31'answersopen, the midnight row, before and after).ltewiden without knowing the column type, as their bounded rule always has ($lte daybecomes "before the next day" on any column). On the last day they compile "has a value" the same way, so a bare-day$lte '9999-12-31'on a text or number field answers every non-null row there.formula'scheckand the draft preview, which see the value, admit an instant and compare anything else as written. The SQL drivers and the engine'shavingscope the rule todatetimecolumns and are exact.$null. The remote lowering assigns lowered keys into one operator map, so the new$null: falseshares a key with an author's own$nullbeside a last-day$lte, as its existing$lteto$ltand$betweento$gtelowerings already share theirs (the memory and mongo translators assemble such writes into$andbranches; the remote path does not). Read from the code, not reproduced; noted, not filed.having/ per-aggregationfilter,$betweenon a missing value. Anullaggregate value passes a$betweenthere, whatever its bounds (neitherordered(null, …)comparison is true). Unchanged by this PR; read from the code, not reproduced.mysqldin this container; theTemporal Conformance (live PG + MySQL)job runs the new kit rows on MySQL 8.0. The real-mongod half of the mongo conformance matrix is opt-in (OS_TEST_MONGODB_MEMORY_SERVER_ENABLED) and was not run; the mongo translator is pinned server-free inmongodb-filter.test.ts.nextUtcCalendarDay's answer as a string stops compiling (3 of the 16 in-repo sites did). Declaredyes (widening), not(narrowing): no input the platform accepted is refused, nothing is removed or renamed. The changeset states the one-line handling for such a caller.datetime.MemoryAnalyticsServiceconverts a comparand to the field's storage form before itslterow asksnextUtcCalendarDay, so on a DECLAREDdatetimethe helper sees an instant and declines, on every day. Measured on the built@objectstack/driver-memoryat this head, rows at2026-07-28T10:00Zand2026-07-27T10:00Z,where { created_at: { $lte: '2026-07-28' } }: undeclared field, both rows (the SQL echo compiles the half-open bound at'2026-07-29'); declareddatetime, only the 07-27 row (the echo compiles an inclusive bound at'2026-07-28T00:00:00.000Z'), whilefind()answers both. Not the last-day defect, so not fixed here. → filed by the seat as [finding] driver-memory analytics: a cubewhere$ltebare day on a declareddatetimefield drops the rest of that day —comparandsForconverts to storage form before the whole-day rule runs #20661.Generated by Claude Code