Repository navigation
fix(driver-sql,service-analytics): bucket the ISO week natively on SQLite, and the SQL echo refuses a bucket SQLite cannot run - #21629
Conversation
…day rule SQLite's capability row now advertises `week`, and `buildDateBucketExpr`'s SQLite arm renders `YYYY-Www` through the `'-3 days', 'weekday 4'` date modifiers instead of `%V`, which libSQL's bundled SQLite 3.45.1 lacks. Claude-Session: https://claude.ai/code/session_017ErfyP2Rx7XWHJA27QjyUi Co-authored-by: Claude <noreply@anthropic.com>
…et on libSQL and sql.js R2: the remote face's inherited `dateBucketSql` row is now pinned by running the rendered expression on a libSQL `file:` client beside the local face. The pins that held SQLite's `week` refusal move to a client driver-sql does not model, which still declines every granularity. Claude-Session: https://claude.ai/code/session_017ErfyP2Rx7XWHJA27QjyUi Co-authored-by: Claude <noreply@anthropic.com>
…e statement for On a SQLite datasource the ObjectQL face's echo no longer prints `date_trunc(...)`, which SQLite refuses. With a non-UTC timezone the dry run answers NOT_IMPLEMENTED / 501 as a declared refusal, and the query answer carries no `sql`. Comments that quoted SQLite `week` as bucketed in memory, or PostgreSQL's datetime bucket text as its only bucket, are corrected (objectql engine.ts, driver-mongodb, the analytics strategy types and the data-modeling queries page). Claude-Session: https://claude.ai/code/session_017ErfyP2Rx7XWHJA27QjyUi Co-authored-by: Claude <noreply@anthropic.com>
…no connection
The refusal is raised while the statement is built, so no connection is
needed, and a `{ server }` object is not a connection config knex types.
Claude-Session: https://claude.ai/code/session_017ErfyP2Rx7XWHJA27QjyUi
Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 4 package(s): 9 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 4 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 29 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 71e778ffd34cc716515ac1da0aecfcba9eefa371 && git checkout 71e778ffd34cc716515ac1da0aecfcba9eefa371
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 045b946256d988653fdca185c7fd33d6d86bd78d 0e7e975a32bce48ea370efbff7e786f2e4a4e1af && git checkout -B drift-repro 045b946256d988653fdca185c7fd33d6d86bd78d && git merge --no-ff 0e7e975a32bce48ea370efbff7e786f2e4a4e1af
node scripts/docs-audit/affected-docs.mjs --json 045b946256d988653fdca185c7fd33d6d86bd78d
|
ACCEPT — PR #21629 at head
|
Brings in objectql-echo-date-bucket.test.ts as main now has it (#21629), the 17th OS_TEST_POSTGRES_URL reader, so its header clause can be corrected in the PR that makes it false. Claude-Session: https://claude.ai/code/session_01T9u38rswFp5Rw8DswRUReJ Co-authored-by: Claude <noreply@anthropic.com>
…t decided them (stage 12 of objectstack-ai#20595) (objectstack-ai#21635) Part of objectstack-ai#20595 Clause-②: no ## What changed Stage 12 of the `domain:engine` lane of the dead-citation sweep: `packages/formula/**`, comment and docblock prose only, per the claim (`5973031304`). Stages 1 to 11 landed as `a7d9768ec`, `d150c3039`, `4bf4e7e70`, `13a24ece2`, `db0cf2231`, `85986144c`, `48fa7a381`, `c205b6c35`, `c98a72d69`, `fd5a1cd59` and `f97660cdd`. objectstack-ai#20595 stays open: the lane's other remaining package, `metadata-fs` (2 census sites after this stage), is not touched here, and neither are the test-string sites the card carries for a widened stage. Every comment or docblock site in the package that cited a tracker number answering 404 is rewritten in ruling C+D's form C (record `5749154545` on objectstack-ai#19123): the ADR when one records the decision, otherwise the commit in this repository's history that made it. That is **6 sites on 6 lines in 2 files, all citing one number (objectstack-ai#6290), all re-anchored to one commit, `e9b526597`**: - **4 census sites** (4 lines, `src/cel-engine.ts` :120, :147, :160, :173): the whole `allocated-but-absent` population of the gate's own census in this package at the base; - **2 test-comment sites** (2 lines, `src/validate.test.ts` :933 and :958), which the census defers by its test-file exclusion. Same number; - **outside the census glob, inside the claimed surface: none.** `README.md`, `package.json`, `tsconfig.json`, `tsconfig.test.json`, `test-typecheck-debt.json` and `vitest.config.ts` carry 10 citations between them, and all 10 resolve on the enumerated board (below); - **dead comment ids: none.** The package's only ten-digit id, `5865693155` (the ruling record on objectstack-ai#20399, at `matches-filter.ts:808` and `matches-filter-empty-operator.test.ts:8`), answers 200. **Anchors: 1 number, by commit; 0 by ADR, 0 by repository qualifier; 1 sha.** `e9b526597` is the anchor the `packages/lint` sweep (`aa23e2c8f`) already chose for the same number; it is reused here and re-proven below for this package's sentences. Only comments changed. Both files keep their line counts (6 lines out, 6 in, plus the changeset), so no line citation into either file moves. No code token moves (the guard below). All 12 changed lines open with a comment marker. **No citation number is added**: the `+` lines carry no tracker number at all, and their only new nine-hex span is `e9b526597`, 6 times. **A `patch` changeset**: 2 of the 4 rewritten non-test lines are in the published `dist` (the `SCOPE_ROOTS` docblock in the `.d.ts`, and one `//` line inside that list that esbuild keeps in the JavaScript), and `dist` is not byte-identical with the base text (see Changeset). ## H0: the package and its size The gate's own `node scripts/check-issue-citations.mjs --census --json` at base `045b94625` (the before run below), `allocated-but-absent` per remaining `domain:engine` package: | package | before | after this stage | |---|---|---| | `formula` | **4** | **0** | | `metadata-fs` | 2 | 2 | | `drivers/driver-mongodb`, `drivers/driver-turso`, `metadata-core`, `core`, `metadata-protocol`, `objectql`, `metadata`, `drivers/driver-sql`, `drivers/driver-memory`, `drivers/driver-sqlite-wasm`, `plugins/plugin-pinyin-search`, `platform-objects` | 0 each | 0 each | The lane total goes 6 to 2. `formula` reads 4, as at stage 11's head census (`b69c176e9`): the 4 sites, the one number and the one file the dispatch named. So the stage went ahead. ## Census: `formula`, before and after **Instrument (A1).** The gate's own `node scripts/check-issue-citations.mjs --census --json`, read-only and unchanged. The count is its `allocated-but-absent` findings under `packages/formula/`. | reading | tree | board | whole-repo `allocated-but-absent` | sites | lines | files | numbers | |---|---|---|---|---|---|---|---| | before | base `045b94625`, run 20:12:19Z to 20:16:21Z | enumerated, 195 pages, frontier objectstack-ai#21629, 19,450 records | 119 | **4** | 4 | 1 | 1 | | after | `7a520db9d`, run 20:22:39Z to 20:25:58Z | enumerated, 195 pages, frontier objectstack-ai#21630, 19,451 records | 115 | **0** | 0 | 0 | 0 | | head | `b89eb86cb`, run 20:48:23Z to 20:51:45Z | enumerated, 195 pages, frontier objectstack-ai#21633, 19,454 records | 115 | **0** | 0 | 0 | 0 | The whole-repo drop is 4, and the before and after finding sets differ by exactly the 4 rows of this package, removed; none was added. `resolves` (35,759), `resolves-as-pull-request` (2,383) and `cross-repo-unjudged` (1,254) did not move. The head's only later commit is the changeset; the head run's finding set is identical to the after run's, line numbers included. **Supplementary instrument, the whole package.** The census reads neither test files nor strings nor files outside `src`. A second reading runs the gate's own exported `extractCitations` (whole-file and comment-prose projections) over every tracked file in the package (69) and classifies each citation with the gate's `classifyCitation` against one board enumerated by the gate's `enumerateBoard` (195 pages, frontier objectstack-ai#21629, 19,450 records, read from 20:17:47Z to 20:21:16Z), the same board for both readings. | reading | citations | dead | src comment | test comment | test string | other files | changelog | |---|---|---|---|---|---|---|---| | before, `045b94625` | 763 | **12** | 4 | 2 | 2 | 0 | 4 | | after, `7a520db9d` | 757 | **6** | 0 | 0 | 2 | 0 | 4 | The citation count drops by 6, the 6 rewritten sites; no respelling stays a citation. The live counts did not move (src comment: 229 resolve as issues, 6 as pull requests, 1 cross-repo; test comment: 148, 11 and 1; test string: 100, 1 and 2; files outside `src`: 10 resolve as issues). A third, raw reading (every `#` followed by 2 to 6 digits, whatever surrounds it, `CHANGELOG.md` aside) counts 527 before and 521 after: also a drop of 6. **Single reads** over the issues endpoint (20:21:27Z): objectstack-ai#6290 answers 404; the controls objectstack-ai#6584, objectstack-ai#6713, objectstack-ai#6146, objectstack-ai#6798, objectstack-ai#6585, objectstack-ai#6711, objectstack-ai#16412 and objectstack-ai#3447 answer 200. Comment `5865693155` answers 200, and the control `5973031304` (the claim) answers 200. No `issuecomment` or `discussion_r` link stands in the package (git grep exit 1). ## Per-number table `src` counts census sites, `test` the test-comment sites. | number | src | test | anchor | kind | source | what it decided | |---|---|---|---|---|---|---| | `objectstack-ai#6290` | 4 | 2 | `e9b526597` | commit | reused (`packages/lint` sweep `aa23e2c8f`), re-proven here | `current_user`, ADR-0068 D1's canonical user root, joins `SCOPE_ROOTS`, and the field-level `*When` rejection stops falling out of that list's omission and becomes `@objectstack/lint`'s own rule with its own prescription. It wrote the `'current_user'` entry with its comment, the block comment after the list, and both `validate.test.ts` docblocks (the squash of PR objectstack-ai#6584) | The proof, per the earlier stages' standard: - `git rev-parse --disambiguate=e9b526597` matches exactly one commit, `e9b5265970029092a8b7124f8c2df98aa6ac0b15`. - `git merge-base --is-ancestor` puts it under the base `045b94625` and under `origin/main` `045b94625` (exit 0 both; exit 0 is self-proving, and the repository is not shallow). - It names objectstack-ai#6290 in its message (the subject's suffix) and in its diff (18 diff lines). - `git blame` at the base puts 5 of the 6 changed lines on it. The sixth, `cel-engine.ts:120`, was written by `d5e9f6e7c`, a later change that cites objectstack-ai#6290 as the change in which `current_user` arrived. That arrival is `e9b526597`'s diff: the `'current_user',` entry added to `SCOPE_ROOTS`. - No ADR or ruling record names objectstack-ai#6290 (git grep over `docs/adr`, exit 1). ADR-0068 D1 records that `current_user` is the canonical spelling, a decision these sentences already cite by its own ADR id. It does not record the list change that the number stood for. ## Wordings to check All 6 rewrites swap a tag in place, in stage 1's forms: - `(#N)` became `(commit SHA)` at `cel-engine.ts:147` and `:160`. - `[#N]` became `[commit SHA]` at `validate.test.ts:933` and `:958`. - 「`current_user` arrived in objectstack-ai#6290」 became 「`current_user` arrived in commit e9b5265」 (`cel-engine.ts:120`). This is the `packages/lint` sweep's wording for the twin sentence in `validate-expressions.ts`. - 「Before objectstack-ai#6290 that」 became 「Before commit e9b5265 that」 (`cel-engine.ts:173`). **A cross-package quotation stays verbatim.** `packages/lint/src/validate-expressions.ts:769` quotes this package's `SCOPE_ROOTS` entry as "the last one this list was missing". That phrase is all on `cel-engine.ts:146`, which this stage does not touch, and the rewritten `(commit e9b5265)` is on the next line. No other file cites a line of `cel-engine.ts` (git grep for `cel-engine.ts:` followed by a line number, no hit). **No reflow.** No line was reflowed, so `:120`, `:173` and `validate.test.ts:958` are now longer than their block's wrap. `eslint.config.mjs` declares no line-length rule, and a reflow would move neighbouring lines. ## Sites left - **In comments (src, test, outside the glob): none.** - **String literals: 2 test-string sites, 1 number**: the `it` titles at `validate.test.ts:950` and `:964` (objectstack-ai#6290). Strings are outside this stage's surface, and the number's anchor is in this stage's table. Non-test strings cite no dead number. - **Outside `src`:** the release-owned `CHANGELOG.md` names objectstack-ai#6290 on 4 lines (2875, 2946, 4350, 4421); left. - **Outside this package:** `packages/lint/src/validate-expressions.test.ts` cites objectstack-ai#6290 on 9 lines. That is another lane's test file, and the `packages/lint` sweep's own PR notes it. ## Mechanical guard: no code token moves The guard compares base `045b94625` against the tree over both touched files, with TypeScript 6.0.3, to the earlier stages' two-reading specification. Stage 11's guard script was a scratch file, so it was rewritten here to that specification and proven with the controls below. - **Reading 1**: the parser's leaf nodes, from a `forEachChild` walk. Comments are trivia there, and JSDoc is never visited. A leaf that is not itself a token is re-scanned with trivia skipped. - **Reading 2**: the full token stream in parser context, from a `getChildren` walk, with JSDoc nodes skipped. String, template and numeric literals are compared in full on both readings. Results, at `7a520db9d` (the later commit touches neither file): - Real run: 13,628 base tokens, **0 files with a token change** (exit 0). - Comment controls: 「Membership of THIS list can.」 to 「… CAN.」 (`cel-engine.ts`) and 「Pinned as behaviour」 to 「Pinned as BEHAVIOUR」 (`validate.test.ts`). 0 files changed (exit 0 each). - Positive control, an identifier (`export const SCOPE_ROOTS` to `XSCOPE_ROOTS`, `cel-engine.ts`): DIFFER on both readings (exit 1). - Positive control, a string literal (`toContain('current_user')` to `'current_useR'`, `validate.test.ts`): DIFFER on both readings (exit 1). - Positive control, a template literal (the `${root}.x` template to `${root}.y`, `validate.test.ts`): DIFFER on both readings (exit 1). - Positive control, a numeric literal (`maxAstNodes: 256` to `257`, `cel-engine.ts`): DIFFER on both readings (exit 1). Each mutation went through `scripts/ablation-replace.mjs` (wrap mode; the anchor hit 1 before and 0 after, and the blob changed). It ran under a shell trap that restores by absolute path from `HEAD`. Each restore was proven equal to its `HEAD` blob (`6fa1738be716`, `832c4f069ec7`), and afterwards `git diff HEAD` was empty and the tree clean. ## Changeset: `patch` (`dist` measured) `files[]` is `dist`, `README.md` and `CHANGELOG.md`, and the package is not private. One script ran under the shared verify lock (VERDICT command-exit 0, held 75s, shared-box seconds), at `7a520db9d`. It built the dependency closure first (`pnpm --filter '@objectstack/formula^...' build`, exit 0), then ran the package's own `build` (tsup and `check-dts-emitted`) three times, exit 0 each: - **Leg 1**, the head text: 6 `dist` files hashed (`index.js`, `index.mjs`, their sourcemaps, `index.d.ts`, `index.d.mts`). 2 of the 4 rewritten non-test lines appear verbatim in `dist`: - the `SCOPE_ROOTS` docblock line (`cel-engine.ts:120`) in `index.d.ts` and `index.d.mts`; - the `// (commit e9b5265).` line inside the list (`cel-engine.ts:147`) in `index.js` and `index.mjs`. The other 2 (`:160`, `:173`) sit in a block comment the build drops. - **Leg 2**, the base text put back in `cel-engine.ts` (proven equal to its base blob `667f87448eeb`, written to the tree only): 4 of the 6 files differ from leg 1 (`index.d.ts`, `index.d.mts`, `index.js`, `index.mjs`); the two sourcemaps do not. `scripts/ablation-dist-preflight.mjs` finds the base marker 「arrived in objectstack-ai#6290) is silently unreported at」 in the 2 declaration files (exit 0). - **Leg 3**, after the proven restore (equal to its `HEAD` blob `6fa1738be716`, `git diff HEAD` empty, porcelain empty): all 6 files are byte-identical to leg 1. The preflight's `--absent` reading exits 0 with a clean tree. So the build is deterministic, and the difference is the rewrite. So the rewrite ships. `.changeset/20595-formula-provenance-anchors.md` declares a `patch` for `@objectstack/formula`, comment text only, with the claim's `Clause-②: no` line. The anchor is a commit, so the changeset names no ADR, repository qualifier or bracketed substitution. It says which published files carry the reworded text, as measured above. The changeset commit touches no file under `packages/formula`. ## Gates (head `b89eb86cb`) - **Derived gates:** `node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands` at `b89eb86cb` (3 paths against merge base `045b94625`) derived 63 commands. All 63 ran (20:36:36Z to 20:47:06Z, after the workspace build), each exit code captured before any pipe: 63 exit 0. `--ran` reports 「63 derived, 63 run, 0 NOT-MEASURED, 0 UNRUN」 (a derived zero) and exits 0. - The PM's lead derivation (49 commands, one path) is a subset. The extra 14 are: - the eight families the `.changeset/` path adds: the ADR-0087 registration and empty-changeset pairs, `check:objectui-changeset`, `check:pm-changeset-deadline-census` and two release self-tests; - `check:type-check-coverage` and `check:type-check-debt`; - four gates whose sources name the touched files: `check:engine-double-contract`, `check:objectql-double-limit`, `check:query-options-erasure` and `check:where-matcher`. - **Named readings:** - `node scripts/check-issue-citations.mjs` exits 0 (「no issue citations added against 045b946」). - `pnpm check:issue-citations` exits 0 (its self-test, 173 cases, 9 batteries). - `pnpm check:doc-authoring` exits 0 (the sibling-package prose-id baseline holds, no growth). - `pnpm check:nul-bytes` exits 0 (9,998 files, no raw control bytes), and a control-byte grep over the 3 changed files finds none (exit 1). - The changeset gates exit 0: `check-adr-0087-registration` (「1 non-breaking changeset(s) seen」), `check-empty-changeset` (「1 declaring changeset(s) added」), `check-changeset-no-major` (「no `major` bump」), and `check:changeset-gate-self-tests`. The Clause-② level axis of `check-changeset-no-major` reads the pull request body, so it does not apply to a local run; that reading is CI's. - **Build, tests and typecheck, under the verify lock**, at `b89eb86cb`: - The workspace build (`turbo run build --filter='./packages/*' --filter='./packages/*/*' --concurrency=2`): VERDICT command-exit 0, held 213s, shared-box seconds; 71 of 71 tasks, 17 cached. - The tests and typecheck: VERDICT command-exit 0, held 19s. `pnpm --filter @objectstack/formula test`: 43 test files pass, 1,257 tests pass. `pnpm --filter @objectstack/formula typecheck` (`tsc --noEmit` and `check:test-typecheck` over `tsconfig.test.json`) exits 0. - `tsc --listFilesOnly` puts both touched files in a program: `cel-engine.ts` in both configs, and `validate.test.ts` in `tsconfig.test.json`, whose program holds all 43 tracked test files (`tsconfig.json` excludes them). - No importing package owes a run, because the declaration files change only in comment text. - **Lint, as a proven narrowing, at `b89eb86cb`:** - eslint ran with inline config disabled (`--format json`) over the 2 touched files plus `dist/index.js` as the control. - 3 results: 0 errors, and 1 warning, which is the control's ignore notice. Neither touched file is reported ignored, and `--print-config` resolves a config for each. - `eslint.config.mjs` never enables type-aware linting (its lines 327 and 328 say so; `--print-config` shows no `parserOptions.project` for either file), so a comment edit cannot move the verdict on an untouched file. - The repo-wide `pnpm lint` is CI's run. ## Acceptance notes - **Base, and no merge.** The dispatch read `origin/main` at `045b94625`, and the worktree was cut there. Every reading above is on this branch's own tree. Just before this PR was opened, `main` moved one commit, `a4f0cb0a4`: a `service-automation` change, with its changeset and one lockfile line. - It touches neither `packages/formula`, `check-issue-citations.mjs` nor `dispatch-gates.mjs`. - A local `git merge-tree` of the head with it is clean (exit 0), and the anchor `e9b526597` is under it too (`--is-ancestor`, exit 0). - The branch is not merged with it: the gate derivation reads the three-dot change set against the merge base `045b94625`, and CI and the merge queue run on the merged ref. - **History.** The repository is not shallow (`git rev-parse --is-shallow-repository` answers false), so no deepening was needed before the blame, ancestry and history readings. - **The same dead number outside this package**, each left to its own carrier: `formula`'s 2 test-string sites (above); `CHANGELOG.md` (release-owned); `packages/lint`'s test file (another lane). - **Wording only:** no line without the number was changed. --- _Generated by [Claude Code](https://claude.ai/code/session_017ErfyP2Rx7XWHJA27QjyUi)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
… buckets in memory, on every dialect (objectstack-ai#21630) (objectstack-ai#21645) Fixes objectstack-ai#21630 Clause-②: no ## What changes One rule for the ObjectQL face's SQL echo (`ObjectQLStrategy.generateSql`), on every dialect, as triage ruled on the card (5973378431): whenever the engine buckets a date dimension in memory because the query carries a non-UTC `timezone` (ADR-0053 Phase 2, D2; `tzRequiresInMemory` in objectql's `engine.ts`), the echo answers the refusal PR objectstack-ai#21629 introduced on SQLite, `NOT_IMPLEMENTED` / 501 with `refusal: true`. No `date_trunc` text for that case on any dialect. - The check runs in `dimExpr` **before** the `dateBucketSql` hook is asked, on the test the engine makes (`timezone` set and not `'UTC'`). So it holds on PostgreSQL, MySQL and SQLite, and also on a host that wires no hook and on a non-SQL driver: the engine buckets a non-UTC zone in memory whatever the driver. - `sqliteBucketEchoRefused` becomes `bucketEchoRefused`, with two arms that share one envelope: the in-memory zone (every dialect, new message), and SQLite where no driver expression answers at UTC (the earlier no-hook arm, message unchanged). - UTC and an unset `timezone` are untouched: each driver's own expression (`to_char` / `date_format` / `strftime`). - `AnalyticsServiceConfig.dateBucketSql`'s TSDoc said "a host that wires nothing keeps the echo's representative `date_trunc`". It ships in `dist/index.d.ts` (measured, line 1250 after build), and this change makes it false for a non-UTC zone, so it now says when the hook is asked. Comment only. It sits outside the claim's listed file surface; see the scope note below. - Changeset: `@objectstack/service-analytics` patch, `Clause-②: no`. No driver change, no `packages/objectql` edit, no `packages/spec` edit, no zone-aware expression ("Not this card" in the ruling). ## Reproduced first, on `main` PostgreSQL 16.14, a throwaway server with `TimeZone` = `Asia/Shanghai`, process `TZ=America/New_York`, the pin's five rows, at `main` `7b07749f05`. The echo text is the same in the `/analytics/query` answer's `sql` and in the `/analytics/sql` dry run (`analytics.generateSql`, the body that route answers with). | query `timezone` | face rows | echo printed | that echo, run on PostgreSQL | |:--|:--|:--|:--| | `Asia/Shanghai`, month | `2026-01` 20, `2026-02` 8, `2026-03` 10, `2026-04` 5 | `date_trunc('month', closed_at)` | `2025-12-31T16:00:00.000Z` 20, `2026-01-31T16:00:00.000Z` 8, `2026-02-28T16:00:00.000Z` 10, `2026-03-31T16:00:00.000Z` 5 | | `America/New_York`, month | `2026-01` 27, `2026-02` 1, `2026-03` 10, `2026-04` 5 | the same text | 20 / 8 / 10 / 5, on the session zone's calendar: other groupings than the face | | `UTC` or unset, month | `2026-01` 27, `2026-02` 1, `2026-03` 10, `2026-04` 5 | `to_char(("closed_at")::timestamptz AT TIME ZONE 'UTC', 'YYYY-MM')` | identical to the face | `week` gave the same shape (non-UTC echoed `date_trunc('week', closed_at)`). ## Both faces after the change Measured on the same server at `ff399dae2d`: - **`/analytics/sql`** refuses with `NOT_IMPLEMENTED` / 501 and `refusal: true`, and `declaredRefusalMessage(err)` returns `err.message`. That is the same envelope the SQLite arm answers, raised through the same error shape. - **`/analytics/query`** returns the same rows as before for both zones (table above, unchanged), and its answer has no `sql` key, because `execute()` already swallows an echo refusal. - **Not re-measured:** the HTTP envelope through the dispatcher. The route forwards the error object the SQLite arm already raises, so it was not measured again. ## Pins `objectql-echo-date-bucket.test.ts`: - **Turned around.** The PostgreSQL cell's `FALLBACK: a non-UTC timezone ... keeps date_trunc` asserted `date_trunc('month', closed_at)`, a statement the engine did not run. That case and the SQLite `REFUSAL` case are now one case that runs on every live cell, for `Asia/Shanghai` and `America/New_York`. It asserts: - the rows, per zone; - that `res.sql` is undefined; - the dry run's code, status and `refusal`; - a UTC control that now equals `driver.dateBucketSql(...)` (it was `not.toContain('date_trunc')`). - **New: a dialect matrix.** `SqlDriver` is built for better-sqlite3, pg and mysql2 and never connected; its `dialectName` and `dateBucketSql` answer from the config alone. It is wired the way the plugin wires its two hooks. On each dialect, non-UTC × {month, week} refuses, and UTC or unset × {month, week} equals that driver's own expression. This pins MySQL without a server. - **New:** a host that wires no hook refuses a non-UTC zone. - **Kept:** the no-hook UTC `date_trunc` case and the no-hook SQLite refusal. Other `service-analytics` tests that assert the fallback text, classified. All three stay: none sets a `timezone` and none wires the hook, so each asserts the UTC path with no hook. | file | assertion | disposition | |:--|:--|:--| | `dataset-selection-window.test.ts:357` | `date_trunc('month', created_at)` | stays: the dataset path resolves `selection.timezone ?? context.timezone ?? 'UTC'` to UTC, and no hook is wired | | `objectql-daterange.test.ts:423` | `date_trunc('month', close_date)` | stays: no timezone, no hook | | `cube-authored-format-granularity.test.ts:247` | `date_trunc('month'` regex | stays: no timezone, no hook | No assertion was deleted. ## Ablation **Predicted first.** Revert the in-memory refusal to `main`'s fallback (`date_trunc` unless SQLite). Then 11 cases turn red: - the live PostgreSQL REFUSAL cases (2); - the matrix's postgres and mysql non-UTC cases (8); - the no-hook non-UTC case (1). The SQLite cases and every UTC case stay green. **Observed.** At `ff399dae2d` (the fix committed first), through `scripts/ablation-replace.mjs` in wrap mode, with live PostgreSQL: - the anchor went from 1 hit to 0, and the blob from `1ebcf8942983` to `b4081144edee`; - the run answered **11 failed, 37 passed (48)**: exactly the predicted set. **Restore proven:** - the blob is back to `1ebcf8942983`, which is the HEAD blob; - `git diff HEAD` is empty, and `git status --porcelain` is empty. There is no build leg: the pin imports the strategy from `src` by relative path. ## Tests and gates, at head `267d2202ae` (after merging `origin/main`) - **Full suite.** `pnpm --filter @objectstack/service-analytics exec vitest run --maxWorkers=2`, with `OS_TEST_POSTGRES_URL` set (PostgreSQL 16.14) and `TZ=America/New_York`: 176 files passed; 4451 tests passed, 2 skipped. - **The pin, verbose:** 48 passed. The live PostgreSQL cell ran. - **Typecheck.** `pnpm --filter @objectstack/service-analytics typecheck` is clean. `tsc --listFiles` includes the pin. - **Build.** The dependency closure (`--filter '@objectstack/service-analytics^...'`) and the package itself. - **Derived gates.** `node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands` derived 64 commands, and all 64 exited 0. `--ran` reconciled: 64 derived, 64 run, 0 NOT-MEASURED, 0 UNRUN. The dist-reading gates (`check:dts-closure`, `check:dual-build-cjs-loads`, `check:sourcemap-no-sources-content`, `check:published-files`, `check:lean-entry-closure`) were re-run after the package's own build, and all exited 0. - **Lint, a declared narrowing.** These three facts together make it a measurement: 1. eslint's own config admitted the 3 changed `.ts` files, with none ignored, under `--no-inline-config` (as `pnpm lint` runs). 2. `--format json` counted 3 files, 0 errors, 0 warnings. 3. `eslint.config.mjs` enables no type-aware linting (no `parserOptions.project`, no typed rules), so this diff cannot move the verdict on a file it does not touch. - **NOT MEASURED: MySQL live rows,** because there is no MySQL server in the container. The matrix pins the echo there. The rows of a non-UTC query are bucketed by the engine in memory on every dialect. ## Overlap with objectstack-ai#21577 Open draft PR objectstack-ai#21577 edits this pin's header docblock (the CI-provisioning sentence about `OS_TEST_POSTGRES_URL`). This PR's docblock edits start one line below that sentence and leave it untouched. A local `git merge-tree` of this head with objectstack-ai#21577's head `6e12d6934a` exits 0, and the merged docblock keeps both intents. Once objectstack-ai#21577 lands, CI runs the live PostgreSQL REFUSAL case. ## Docs and skills - **`content/docs/**`** (outside `releases/` and `references/`): grepped for the analytics `sql` echo, `date_trunc` and timezone bucketing. No sentence is made false by this change. `data-api.mdx`'s `/analytics/sql` section declares the success shape only. - **`skills/**`:** two sentences name `date_trunc`, `objectstack-ui/rules/dashboards.md:326` and `objectstack-query/rules/aggregation.md:82`. Both are already carried by objectstack-ai#21588, the first in its body and the second in comment 5973137901. Neither is made false by this change, and neither is edited here. ## Acceptance notes - **Scope note.** The `AnalyticsServiceConfig.dateBucketSql` TSDoc fix in `analytics-service.ts` is outside the claim's listed file surface. It is the published sentence this change made false, in the same package and the same defect class, and the edit is mechanical. No open PR touches the file. The seat records the surface addition. - **`date_trunc` is still reachable at a UTC or unset `timezone`,** where the hook answers nothing and the dialect is not SQLite. That arm is outside the ruling's "the engine buckets in memory (a non-UTC `timezone`)", so it is not widened here: - On a **driver-memory** datasource it was measured at the service seam: the engine buckets in memory (the driver advertises no `queryDateGranularity`) and answers `2026-01` keys, while both faces echo `date_trunc('month', closed_at)`, a statement nothing ran. - MongoDB takes the same code path, with no `dateBucketSql` and an unknown dialect. Not measured: there is no server. - The no-hook pin keeps this arm on purpose. - Reported to the seat as a finding. - **`plugin.ts` comment.** The `dateBucketSql` bridge comment in `plugin.ts` ("`undefined` keeps the echo's representative `date_trunc(…)`") has been inexact on SQLite since PR objectstack-ai#21629. It is an internal comment, not in the published types, and this change does not touch it. Carrier: none. --- _Generated by [Claude Code](https://claude.ai/code/session_01DiCSbmJrkzNhuEAier4VoJ)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
…bjectstack-ai#21677) Fixes objectstack-ai#21588 Clause-②: no Three sentences in two published skills describe the date-bucket engine, and each one named something the runtime does not do. This PR rewrites exactly those three sentences, read arm by arm from the driver code on `origin/main` at `55e6f14f8d`, and nothing else in either file. | File | Line | Before | After | |:--|:--|:--|:--| | `skills/objectstack-ui/rules/dashboards.md` | 321 | `'week'` row: "ISO date of the bucket (`YYYY-MM-DD`)" | "ISO week `YYYY-Www`" | | `skills/objectstack-ui/rules/dashboards.md` | 326–328 | "Postgres `date_trunc`, MySQL `date_format`, SQLite `strftime`, MongoDB `$dateTrunc`, in-memory fallback. All emitted by the analytics service, not the client." | "drivers emit the bucket as a label (`2026-01`), not an instant: Postgres `to_char`, MySQL `date_format`, SQLite `strftime`, MongoDB `$dateToString`, in-memory `bucketDateKey`." | | `skills/objectstack-query/rules/aggregation.md` | 82–85 | "pushes bucketing down to the driver (`DATE_TRUNC` etc.)" | "pushes bucketing down to the driver (`to_char` / `date_format` / `strftime` / `$dateToString`, never `date_trunc`)" — the push-down / in-memory-fallback clause and "**including the column keys**" are kept as they were | The ruling on the card (triage comment 5969875145) fixed the engine sentence and said no other sentence in `dashboards.md` moves; the engine seat's carrier addition (5973137901) measured the `'week'` row and the aggregation sentence as the same family, and the dispatching seat folded all three into this one governed PR (claim comment 5975736098). ## Reading 1 — what each arm emits, read from the code Every arm answers a string label, never a truncated instant; the week label is the ISO week `YYYY-Www` on all five. | Arm | Where | Expression emitted | Example keys | |:--|:--|:--|:--| | PostgreSQL | `packages/drivers/driver-sql/src/sql-driver.ts:6161–6169` (`buildDateBucketExpr`) | `to_char((col)::timestamptz AT TIME ZONE 'UTC', FORMAT)` for a datetime column, `to_char((col)::date::timestamp, FORMAT)` for a `Field.date`; formats `YYYY`, `YYYY-MM`, `YYYY-MM-DD`, `YYYY"-Q"Q`, `IYYY"-W"IW` | `2026-01`, `2026-Q1`, `2026-W23` | | MySQL | `sql-driver.ts:6172–6180` | `date_format(convert_tz(col, @@session.time_zone, '+00:00'), FORMAT)` (bare `col` for a `Field.date`); `%Y`, `%Y-%m`, `%Y-%m-%d`, `%x-W%v`; quarter is `concat(date_format(…, '%Y'), '-Q', quarter(…))` | `2026-01`, `2026-W23` | | SQLite | `sql-driver.ts:6183–6208` | `strftime(FORMAT, ARG)` with `%Y`, `%Y-%m`, `%Y-%m-%d`; quarter from `%Y` and `(%m - 1) / 3 + 1`; week by the Thursday rule, `strftime('%Y', ARG, '-3 days', 'weekday 4') \|\| '-W' \|\| printf('%02d', (cast(strftime('%j', ARG, '-3 days', 'weekday 4') as integer) - 1) / 7 + 1)` (PR objectstack-ai#21629, merged, on `origin/main`) | `2026-01`, `2026-W23` | | MongoDB | `packages/drivers/driver-mongodb/src/mongodb-aggregation.ts:246–275` | `{ $dateToString: { format, date: { $convert: { input: '$FIELD', to: 'date', onError: null, onNull: null } } } }` with `%Y`, `%Y-%m`, `%Y-%m-%d`, `%G-W%V`; quarter is `$concat` of `%Y`, `-Q` and a `$switch` over `%m`. The docblock at `:194` is headed "Labels, not instants — and therefore no `$dateTrunc`" | `2026-01`, `2026-W23` | | In-memory | `packages/core/src/utils/datetime.ts:313` `bucketDateKey` (week via `isoWeekLabelFromCalendarDay`, `:389`); the engine's fallback `packages/objectql/src/in-memory-aggregation.ts:375` delegates to it, and `packages/objectql/src/engine.ts:17615` picks push-down vs fallback from `supports.queryDateGranularity` | string keys `YEAR`, `YEAR-MM`, `YEAR-MM-DD`, `YEAR-Qn`, `ISOYEAR-Www` built from the calendar parts in the reference zone — the writer the drivers' expressions are held equal to (`checkDateBucketParity`) | `2026-01`, `2026-W23` | The rendered dashboard label is the key: `packages/services/service-analytics/src/dimension-labels.ts:321–333` `formatDateBucket` returns a key the writer wrote at that granularity as written (`bucketKeyToCalendarRange(value, granularity) !== null`), and `src/__tests__/dataset-granularity-postprocess.test.ts:66` pins `week: '2026-W29'` through `queryDataset`. So the `'week'` row's "ISO date of the bucket (`YYYY-MM-DD`)" was wrong on every face, and the engine sentence named two expressions no arm emits. ## Reading 2 — token ratchet and line count, before / after `node scripts/check-skills-token-ratchet.mjs` (ceil(utf8 bytes / 4)), measured on the worktree before the edit and at `e381bcd9e1` after it: | File | Tokens before | Tokens after | Bytes | Lines | |:--|:--|:--|:--|:--| | `skills/objectstack-ui/rules/dashboards.md` | `6243 tokens (ceiling 6252; headroom 9)` | `6243 tokens (ceiling 6252; headroom 9)` | 24970 → 24969 | 468 → 468 | | `skills/objectstack-query/rules/aggregation.md` | `1845 tokens (ceiling 2357; headroom 512)` | `1860 tokens (ceiling 2357; headroom 497)` | 7378 → 7437 | 241 → 241 | Per sentence: the `'week'` row 52 → 34 bytes; the engine sentence 185 → 202 bytes over the same three lines; the aggregation sentence 244 → 303 bytes over the same four lines. Net for `dashboards.md`: 0 tokens, 0 lines, −1 byte. No ceiling moved. ## Gates All runs at `e381bcd9e1` (the branch's only commit); each runner log records the sha it started at. - `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` derives 23 commands from the change set (2 paths vs merge base `55e6f14f8`). All 23 exit 0. The 8 `node scripts/check-*.mjs` runs plus `check:doc-formula-expressions`, `check:agent-test-spelling` and `check:corpus-claim-drift` ran under `scripts/pm/os-verify-lock.sh` (VERDICT command-exit 0, held 120s). The remaining 12 `pnpm check:*` scanners ran unlocked — a declared narrowing: the lock's own `--status` text places `check:*` gate scripts outside its coverage, and two consecutive lock calls answered queue-timeout (exit 99, 360s each) behind a holder at 13+ minutes. - `check:doc-formula-expressions` first answered exit 3 PREREQUISITE NOT MET (`@objectstack/formula` / `@objectstack/lint` not built — nothing measured). The production closure (`spec`, `types`, `core`, `client`, `client-react`, `formula`, `sdui-parser`, `lint`) was built under the lock (VERDICT command-exit 0, held 133s; `git status` clean afterwards), and the re-run exits 0: "22 record-scoped formula example(s) across 460 files / 1381 TS blocks judged clean by @objectstack/formula". - `pnpm --filter @objectstack/spec run check:skill-docs` exits 0: "✅ Skill docs in sync". - `pnpm --filter @objectstack/spec run check:skill-examples` exits 0 (run unlocked after a third queue-timeout, same declared narrowing): "✅ 260 prose examples type-check across 3 surface(s) — every marked block parsed, so tsc ran the SEMANTIC pass on all of them". The diff sits outside every fence (dashboards.md fences close at 316 and reopen at 354; aggregation.md's close at 72 and reopen at 95), so no example changed. - `node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --ran` over the 23 commands with their exit codes: "23 derived famil(ies) accounted for — 23 run, 0 NOT-MEASURED (a DERIVED zero — all 23 recorded an exit code and none of them is 3)". The tool warns the tree is 3 commits behind `origin/main` (`417443eb27`, fetched after the runs); re-deriving against that merge-base yields the same 23 commands, and the three incoming commits (objectstack-ai#21662, objectstack-ai#21653, objectstack-ai#21661) touch no `skills/**` path, so the branch was not merged forward for a two-file documentation change. - `pnpm lint` narrowing, with the three pieces of evidence: ① `eslint.config.mjs` `files:` globs cover only `{ts,tsx,mts,cts,js,jsx,mjs,cjs}` (lines 971–1238), so neither `.md` file is in the population; ② `pnpm exec eslint --no-inline-config --format json` over the two files answers 2 files, 0 errors, 1 warning each — "File ignored because no matching configuration was supplied."; ③ the config enables no `parserOptions.project` or typed rules (line 328), so this diff moves no untouched file's verdict. - No package is touched, so no build closure (①) and no package test / typecheck (②) is owed; `check:nul-bytes` is among the 23. ## 维护者速读(草稿) **改了什么** — 两个已发布技能里描述日期分桶引擎的三句话。dashboards 规则的「Engine support」句原说 Postgres 用 `date_trunc`、MongoDB 用 `$dateTrunc`、由 analytics service 发出;改为按五个臂点名驱动真实发出的表达式(Postgres `to_char`、MySQL `date_format`、SQLite `strftime`、MongoDB `$dateToString`、内存 `bucketDateKey`),并写明桶键是标签(如 `2026-01`)不是时刻。同一张表的 `'week'` 行由「ISO date of the bucket (`YYYY-MM-DD`)」改为 ISO 周标签 `YYYY-Www`。aggregation 规则的下推句把 `DATE_TRUNC` 换成同一表达式族。不改任何代码,不改产品行为。 **为什么改** — 技能是 AI 作者读的权威面。写错引擎会让作者按 `date_trunc` 语义(时间戳形的桶键)去比较或解析桶值,而运行时五个臂实际都返回字符串标签;`'week'` 行与运行时每个臂返回的 `2026-W29` 不符。每个臂都在 `origin/main`(`55e6f14f8d`)上逐条读过代码,见上文 Reading 1。 **风险与代价(含回滚)** — 纯文档改动,8 行替换 8 行。token 棘轮:dashboards.md 净 0(6243/6252 不变,行数不变),aggregation.md +15(1860/2357)。23 条派生门禁加 `check:skill-docs`、`check:skill-examples` 全绿。回滚 = revert 本 PR 的单个 commit。 **席位意见** — (留空) **你要做的** — 对这个 Tier H `skills/**` PR 给一次 APPROVED review;之后由 `domain:skills#1` 席位落地。 ## Acceptance notes Noted, not filed (code comments, no behaviour, no carrier): - `packages/services/service-analytics/src/dimension-labels.ts:302` — the `formatDateBucket` TSDoc example list still reads `week → "2026-04-13" (ISO date of the bucket)`, while the body just below (`:327–:333`) returns a `YYYY-Www` key as written and relabels only a raw non-key value as its own day key. Comment drift only; the behaviour is pinned by the tests cited above. - `packages/objectql/src/in-memory-aggregation.ts:59` — the header comment names `date_trunc(...)` as the SQL path's NULL-propagating expression; the SQL path emits `to_char` / `date_format` / `strftime`, whose NULL propagation is the same point. Comment drift only. --- _Generated by [Claude Code](https://claude.ai/code/session_01CB6W87z22K2yjUCDyVrJRk)_ Co-authored-by: Claude <noreply@anthropic.com>
Fixes #21595
Clause-②: no
What changes
(a)
weekon SQLite, indriver-sql. The SQLite capability row now advertisesweek, andSqlDriver.buildDateBucketExpr's SQLite arm renders the ISO 8601 week asYYYY-Www, the label the PostgreSQL arm'sIYYY"-W"IWand the MySQL arm's%x-W%vanswer:It is the Thursday rule.
'-3 days', 'weekday 4'moves any day to the Thursday of its Monday-start week, so that Thursday's%Yis the week-numbering year and its day of year gives the week number. It uses no%Vor%G, and both date modifiers predate every SQLite the platform ships.ARGis the existingsqliteTemporalArg: a bare column for TEXT storage, and the legacy repair for an un-backfilledField.datetime. No zone is read, so aField.datestays its calendar day (H2 holds). Both callers follow without a second edit:aggregate()'s GROUP BY and the publicdateBucketSql()the analytics echo reads.(b) A non-UTC zone on SQLite, in
service-analytics' echo.ObjectQLStrategy.generateSqlno longer printsdate_trunc(...)on a SQLite datasource, since SQLite has no such function. Where no driver expression answers and the executing driver's dialect (sqlDialectFor) issqlite, the echo refuses withNOT_IMPLEMENTED/ 501 andrefusal: true. The shipped composition reaches that branch for a non-UTCtimezone: the engine then buckets in memory on that zone's calendar, and SQLite has no time-zone database. The refusal reaches each door as follows:POST /api/v1/analytics/sqlanswers the refusal in the error envelope.refusal: trueis the declared channel (ApiErrorSchema.refusal) for a 5xx whose prose is addressed to the caller, so the boundaries keep the message.POST /api/v1/analytics/querystill answers the rows.execute()already callsgenerateSqlinside atry, so the answer simply carries nosql. This is the reach the existing cross-field decline has.timezone: 'UTC', or none, the echo still prints the driver's expression. On a dialect the host cannot name, it still prints the representativedate_trunc.Riders.
packages/objectql/src/engine.ts. The ADR-0053 D2 comment abovetzRequiresInMemorynow names driver-sql'sto_char/date_format/strftime. In the same block, the comment's example "partial-support dialects (e.g. SQLite + week)" is false after this change; it now names the Turso remote transport, which advertises none. Comments only: 8 changed lines, 0 non-comment.driver-turso'sREMOTE_FACE_ANSWERSrowdateBucketSql: 'inherited'is now pinned by running it. The remote face renders the expression with no connection, and the pin sends it through the remote face's ownexecute()to a real@libsql/clientfile:client, beside a better-sqlite3 local control. All five granularities answer the local labels, andweekanswers the written-out ISO weeks of the boundary days. The row's text needed no change (H4).Comment corrections carried from #21485's ACCEPT 5971379160.
objectql-strategy.ts,strategies/types.tsand the echo pin's docblock quoted PostgreSQL'sto_char((col)::timestamptz AT TIME ZONE 'UTC', ...)as the bucket expression. Since 440cd32 that is thedatetimeexpression only; each now names(col)::date::timestampfor adate.Measured first: each SQLite client's bundled SQLite
sqlite_version()strftime('%V', '2021-01-03')driver-sql,driver-tursolocal face53@libsql/client0.18.0driver-tursoremote face (file:client)driver-sqlite-wasm53The platform ships three SQLite clients, not two:
SqliteWasmDriveris aSqlDriversubclass that overridesisSqlite, so it inherits this arm. libSQL bundles 3.45.1, so by triage's rule%Vis out, and the expression is version-independent.The expression, measured before it was written
@objectstack/core'sbucketDateKeyon better-sqlite3, libSQL and sql.js alike. The inputs are every day from 1999-01-01 to 2032-12-31, datetimes at 00:00, 12:00 and 23:59:59.999Z on the ISO-year boundary days, the years 0001 / 0049 / 0050 / 0999 / 1000 / 9999, and unparseable and empty values, which answer NULL as the in-memory face does.to_char(d::date::timestamp, 'IYYY"-W"IW')over 1999-01-01..2032-12-31, 12,419 days, 0 mismatches. The boundary days: 2020-12-31 and 2021-01-03 are2020-W53, 2021-01-04 is2021-W01, 2024-12-30 is2025-W01, 2026-12-28 and 2027-01-03 are2026-W53.%x-W%v: NOT MEASURED here, because no MySQL server is available in this container. It is pinned to the samebucketDateKeyby CI's live Temporal Conformance.(b): the declared response contract, read first
/analytics/sql:AnalyticsSqlResponseSchemadeclaresdata: { sql: z.string(), params }overBaseResponseSchema, whoseerroris the refusal envelope. Anullstatement with a reason is not admitted, so it is not the shape, and the contract is not widened. A refusal is admitted, and this door already refuses a cross-field comparison (the [spec]service-analytics' read-scope / Cube filter compilers still refuse$field, so a CEL field-to-field RLS rule 400s on those faces #7598 ruling, "一致的响亮答案,不半渲染")./analytics/query:sqlisz.string().optional(), "Generated SQL (if available)", so an answer without it is admitted.So the shape is the refusal. The status is
NOT_IMPLEMENTED/ 501 rather than a 400: the query is spelled correctly and served, and the gap is the backend's. This is the reasoning driver-sql's own bucket refusal gives (#6212 / #5907), and the/meta/:type/:name/references501 is the precedent forrefusal: true.Fixture triage: pins that held the removed branch
driver-sql/src/sql-driver-date-bucket.test.tscaps.weekis nowtrue, andweekjoins the parity sweep. The #6212 refusal pin moves off SQLiteweekto the population that still reaches it: an unmodeled client (mssql, whose capability row is empty; the refusal is raised before any connection). Code, status and message as before, with the tail nowBucketed here: none (dialect 'mssql').driver-sql/src/sql-driver-date-bucket-storage.test.tsweekjoins both storage forms. One new case coversweekover the legacy mixed-form column (epoch INTEGER, ISO TEXT, zone-naive TEXT, NULL), where the repair is expanded twice.driver-sql/src/sql-driver-temporal-dialect.test.tsweekjoins the dialect-gating granularities, so the legacy repair, the plain column form and the binding count are pinned for the new arm. New assertion: no SQLite arm reads aField.datethrough'localtime'or'utc'(H2).driver-sql/src/sql-driver-21485-date-bucket-calendar-day.test.tsBUCKETED_IN_SQLgainsweek, soaggregate()and thedateBucketSqlecho run it on the date/datetime boundary rows.driver-sql/src/sql-driver-13714-...test.tsweek.driver-sqlite-wasm/src/sqlite-wasm-driver-date-bucket.test.tscaps.weekis nowtrue, andweekjoins the parity sweep on sql.js. Its refusal twin is removed: no granularity reaches that refusal on this driver any more.driver-turso/src/remote-transport-groupby-node.test.tsSqlDriver, the only SqlDriver face that still declines. Themonthasymmetry case is parameterized overmonthandweek. The case that pinned the SQLite face'sweekrefusal tail is removed.qa/dogfood/test/date-bucket-parity-turso.test.tsweekbecause SqlDriver could not run it. It now keeps the real row (which advertisesweek) and makes theweekarm answernull, the incoherent pair a revert of the arm alone would ship.checkDateBucketParitystill names it.service-analytics/src/__tests__/objectql-echo-date-bucket.test.tsweek. The in-memory-granularity FALLBACK case is removed, because its only input was SQLiteweek. The non-UTC case is per cell: on SQLite it pins the refusal (rows served with nosql;/analytics/sqlrefuses withNOT_IMPLEMENTED, 501 andrefusal: true;declaredRefusalMessage(err) === err.message; a UTC control renders). On PostgreSQL it is unchanged (see the finding below). A unit case pins the refusal for a SQLite host that wires no hook.Also swept and left alone:
checkDateBucketParity's real-driver half indate-bucket-parity-conformance.test.tsnow exercisesweekon better-sqlite3 and sql.js with no edit. Itsweek: falsefake is self-contained.timedimension-granularity-driver-alias.test.tsis data-driven and green; see the Acceptance notes.Reverse verification (commit first, then revert only the arm)
Taken at head
59a751f4ad, throughscripts/ablation-replace.mjs(wrap mode: literal anchor, restore armed on exit, INT and TERM). The mutation replaced the whole SQLiteweekblock with the originalcase 'week': return null; // see capabilities note. Anchor 1 to 0, blob5f6b143dbe1ato5e80a2795453. The capability row stayedweek: true.@objectstack/driver-sql, thenablation-dist-preflight.mjs @objectstack/driver-sql "'-3 days', 'weekday 4'" --absent: exit 0, so the arm was out of thedist/that driver-turso, driver-sqlite-wasm, service-analytics and dogfood consume.driver-sql): 3 failed, the threeweekcases.driver-tursoR2): 2 failed,week: libSQL answers the labels the local face answersandweek: libSQL answers the ISO week of each boundary day.driver-sqlite-wasm): 1 failed,granularity=week.service-analytics): 2 failed,closed_on, weekandclosed_at, week.git checkout HEAD -- ABSOLUTE_PATH. The blob after restore is5f6b143dbe1a, equal to HEAD,git diff HEADis empty andgit status --porcelainshows 0 entries. Then a rebuild and the presence preflight: the marker is in 2 built files and the tree reading is clean.Tests
Package suites at head
9328c3c6cc(the merge oforigin/maincame after; its incoming commits touch none of these packages except driver-mongodb, where this diff is comments only):@objectstack/driver-sql, with the live PostgreSQL cell atTZ=America/New_York: 225 files passed, 3 skipped; 4571 tests passed, 104 skipped (the MySQL cells: no server here).@objectstack/service-analytics, with the live PostgreSQL cell: 176 files passed; 4421 tests passed, 2 skipped.@objectstack/driver-turso: 88 files passed; 2373 tests passed, 33 skipped.@objectstack/driver-sqlite-wasm: 36 files passed; 675 tests passed.driver-sql,driver-sqlite-wasm,driver-turso,service-analytics,driver-mongodb(withcheck:test-typecheck) andobjectql(all threetscprograms andcheck:test-typecheck) are all clean.On the merged head
0e7e975a32, after a post-merge turbo build and speccheck:generated(15 of 15 artifacts up to date), every changed pin passed again:mongodb-date-bucket-parityDeclared narrowings, so they read as such:
objectqlanddriver-mongodbsuites were not run in full. Their diffs are comment-only (engine.ts8 changed lines,mongodb-aggregation.ts14 and its test 4, all comment lines), and both packages typecheck.packages/qa/dogfoodwas not run in full; the two date-bucket files it changes were. The rest is CI's Dogfood Regression Gate.The live PostgreSQL was a throwaway 16.14 instance this run started on a random port with server
TimeZoneAsia/Shanghai; it has been stopped and removed.Gates
node scripts/pm/dispatch-gates.mjs --commandswith no paths, at head0e7e975a32: 101 derived (the 58 the dispatch named plus 43 the change set adds). All 101 ran on that head and each exited 0.--ranreconciliation: "101 derived famil(ies) accounted for — 101 run, 0 NOT-MEASURED (a DERIVED zero — all 101 recorded an exit code and none of them is 3)". On the first run at59a751f4ad,check:skill-examplesandcheck:dual-build-cjs-loadsanswered exit 3 (PREREQUISITE NOT MET, unbuilt packages). After a full build both passed, and on the final head all 101 are exit 0.Driver-card ledger,
pnpm check:driver-conformance: before (at BASE5c9138b4b6) and after (at0e7e975a32), both 50 covered cells, 0 in the DEBT ledger, 0 exempt. No new DEBT.Lint, a proven narrowing at
0e7e975a32:.tsfiles, run aspnpm lintruns (--no-inline-config). None was reported as ignored.--format jsoncount: 16 files, 0 errors, 0 warnings.eslint.config.mjsnever enables type-aware linting (noparserOptions.project, no typed rules), so this diff cannot move the verdict on an untouched file.File surface, and what this adds to the claim's
The claim named
sql-driver.ts, thedriver-tursopin, theservice-analyticsecho, its pin and two comment files,engine.tsR1, tests and the changeset. Each addition below is forced by this change, and none is a behaviour change outside the claim:driver-sqlite-wasm,driver-turso'sremote-transport-groupby-node.test.ts, andqa/dogfood's Turso parity tripwire.driver-mongodb's TSDoc on the exportedMONGODB_DATE_GRANULARITIES("driver-sql setsweek: false", which ships in its.d.ts), and its test's comment.content/docs/data-modeling/queries.mdx("SQLite reportsweek: false, for instance").engine.tscomment, in R1's own block.Changeset
.changeset/21595-sqlite-week-bucket.md:patchfor@objectstack/driver-sqland@objectstack/service-analytics.driver-tursoships no code change, so it gets none, and thedriver-mongodbandobjectqlchanges are comments.Acceptance notes
service-analytics/src/__tests__/timedimension-granularity-driver-alias.test.ts's header still says SQLite declinesweek. The test is data-driven and green; the file is outside the declared cross-lane set.queryDateGranularityTSDoc example ({ day: true, week: false }"for a SQLite build whosestrftimelacks ISO week") is still a valid hypothetical, but no shipped SQLite driver declares it now.objectql/src/engine-aggregate-timezone.test.ts's header names native bucketingdate_trunc, R1's drift in a test file. None of these is made false by behaviour in this PR in a way any test reads.skills/objectstack-ui/rules/dashboards.md'sweeklabel row ("ISO date of the bucket (YYYY-MM-DD)"; the key isYYYY-Www) andskills/objectstack-query/rules/aggregation.mdline 82 ("DATE_TRUNCetc.") were inaccurate before this change. They belong with skills(objectstack-ui): the dashboards rule says Postgres buckets with date_trunc; the SQL driver groups by to_char(... AT TIME ZONE UTC) on Postgres #21588, the open card for the samedashboards.mdengine sentence, on a Tier H surface this PR does not touch.date_trunc('month', closed_at); this PR leaves it, and the echo pin's PostgreSQL cell still asserts it. That statement was run on PostgreSQL 16.14 (serverTimeZoneAsia/Shanghai) over the pin's five rows:2026-01-01 00:00:00+08-shaped keys on the session zone's calendar, where the face answers2026-01on the request zone's.timezone: 'America/New_York'the groupings differ too: the echo gives January 20 and February 8, where the face gives January 27 and February 1.date_truncis not a function. That is NOT MEASURED here (no server).Generated by Claude Code