Skip to content

fix(driver-sql,service-analytics): bucket the ISO week natively on SQLite, and the SQL echo refuses a bucket SQLite cannot run - #21629

Merged
objectstack-fleet[bot] merged 5 commits into
mainfrom
claude/issue-21595-sqlite-week-bucket
Oct 3, 2026
Merged

objectstack-fleet[bot] merged 5 commits into
mainfrom
claude/issue-21595-sqlite-week-bucket

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #21595
Clause-②: no

What changes

(a) week on SQLite, in driver-sql. The SQLite capability row now advertises week, and SqlDriver.buildDateBucketExpr's SQLite arm renders the ISO 8601 week as YYYY-Www, the label the PostgreSQL arm's IYYY"-W"IW and the MySQL arm's %x-W%v answer:

(strftime('%Y', ARG, '-3 days', 'weekday 4') || '-W' ||
 printf('%02d', (cast(strftime('%j', ARG, '-3 days', 'weekday 4') as integer) - 1) / 7 + 1))

It is the Thursday rule. '-3 days', 'weekday 4' moves any day to the Thursday of its Monday-start week, so that Thursday's %Y is the week-numbering year and its day of year gives the week number. It uses no %V or %G, and both date modifiers predate every SQLite the platform ships. ARG is the existing sqliteTemporalArg: a bare column for TEXT storage, and the legacy repair for an un-backfilled Field.datetime. No zone is read, so a Field.date stays its calendar day (H2 holds). Both callers follow without a second edit: aggregate()'s GROUP BY and the public dateBucketSql() the analytics echo reads.

(b) A non-UTC zone on SQLite, in service-analytics' echo. ObjectQLStrategy.generateSql no longer prints date_trunc(...) on a SQLite datasource, since SQLite has no such function. Where no driver expression answers and the executing driver's dialect (sqlDialectFor) is sqlite, the echo refuses with NOT_IMPLEMENTED / 501 and refusal: true. The shipped composition reaches that branch for a non-UTC timezone: 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/sql answers the refusal in the error envelope. refusal: true is 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/query still answers the rows. execute() already calls generateSql inside a try, so the answer simply carries no sql. This is the reach the existing cross-field decline has.
  • With timezone: 'UTC', or none, the echo still prints the driver's expression. On a dialect the host cannot name, it still prints the representative date_trunc.

Riders.

  • R1: packages/objectql/src/engine.ts. The ADR-0053 D2 comment above tzRequiresInMemory now names driver-sql's to_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.
  • R2: driver-turso's REMOTE_FACE_ANSWERS row dateBucketSql: '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 own execute() to a real @libsql/client file: client, beside a better-sqlite3 local control. All five granularities answer the local labels, and week answers 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.ts and the echo pin's docblock quoted PostgreSQL's to_char((col)::timestamptz AT TIME ZONE 'UTC', ...) as the bucket expression. Since 440cd32 that is the datetime expression only; each now names (col)::date::timestamp for a date.

Measured first: each SQLite client's bundled SQLite

client package sqlite_version() strftime('%V', '2021-01-03')
better-sqlite3 13.0.3 driver-sql, driver-turso local face 3.53.4 53
@libsql/client 0.18.0 driver-turso remote face (file: client) 3.45.1 NULL
sql.js 1.14.2 driver-sqlite-wasm 3.49.1 53

The platform ships three SQLite clients, not two: SqliteWasmDriver is a SqlDriver subclass that overrides isSqlite, so it inherits this arm. libSQL bundles 3.45.1, so by triage's rule %V is out, and the expression is version-independent.

The expression, measured before it was written

  • On each client: 12,454 inputs per client, with 0 mismatches against @objectstack/core's bucketDateKey on 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.
  • Against PostgreSQL's own arm: PostgreSQL 16.14 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 are 2020-W53, 2021-01-04 is 2021-W01, 2024-12-30 is 2025-W01, 2026-12-28 and 2027-01-03 are 2026-W53.
  • MySQL's %x-W%v: NOT MEASURED here, because no MySQL server is available in this container. It is pinned to the same bucketDateKey by CI's live Temporal Conformance.

(b): the declared response contract, read first

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/references 501 is the precedent for refusal: true.

Fixture triage: pins that held the removed branch

file disposition
driver-sql/src/sql-driver-date-bucket.test.ts caps.week is now true, and week joins the parity sweep. The #6212 refusal pin moves off SQLite week to 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 now Bucketed here: none (dialect 'mssql').
driver-sql/src/sql-driver-date-bucket-storage.test.ts week joins both storage forms. One new case covers week over 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.ts week joins 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 a Field.date through 'localtime' or 'utc' (H2).
driver-sql/src/sql-driver-21485-date-bucket-calendar-day.test.ts SQLite's BUCKETED_IN_SQL gains week, so aggregate() and the dateBucketSql echo run it on the date/datetime boundary rows.
driver-sql/src/sql-driver-13714-...test.ts Comment only. The sweep is data-driven by the capability row and now serves week.
driver-sqlite-wasm/src/sqlite-wasm-driver-date-bucket.test.ts caps.week is now true, and week joins 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.ts The #5240 parity case ("one condition, one wording") compares the remote transport with an unmodeled SqlDriver, the only SqlDriver face that still declines. The month asymmetry case is parameterized over month and week. The case that pinned the SQLite face's week refusal tail is removed.
qa/dogfood/test/date-bucket-parity-turso.test.ts The tripwire made a local driver advertise week because SqlDriver could not run it. It now keeps the real row (which advertises week) and makes the week arm answer null, the incoherent pair a revert of the arm alone would ship. checkDateBucketParity still names it.
service-analytics/src/__tests__/objectql-echo-date-bucket.test.ts The SQLite cell groups week. The in-memory-granularity FALLBACK case is removed, because its only input was SQLite week. The non-UTC case is per cell: on SQLite it pins the refusal (rows served with no sql; /analytics/sql refuses with NOT_IMPLEMENTED, 501 and refusal: 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 in date-bucket-parity-conformance.test.ts now exercises week on better-sqlite3 and sql.js with no edit. Its week: false fake is self-contained. timedimension-granularity-driver-alias.test.ts is data-driven and green; see the Acceptance notes.

Reverse verification (commit first, then revert only the arm)

Taken at head 59a751f4ad, through scripts/ablation-replace.mjs (wrap mode: literal anchor, restore armed on exit, INT and TERM). The mutation replaced the whole SQLite week block with the original case 'week': return null; // see capabilities note. Anchor 1 to 0, blob 5f6b143dbe1a to 5e80a2795453. The capability row stayed week: true.

  • Rebuilt @objectstack/driver-sql, then ablation-dist-preflight.mjs @objectstack/driver-sql "'-3 days', 'weekday 4'" --absent: exit 0, so the arm was out of the dist/ that driver-turso, driver-sqlite-wasm, service-analytics and dogfood consume.
  • Red on every client (the predicted direction):
    • better-sqlite3 (driver-sql): 3 failed, the three week cases.
    • libSQL (driver-turso R2): 2 failed, week: libSQL answers the labels the local face answers and week: libSQL answers the ISO week of each boundary day.
    • sql.js (driver-sqlite-wasm): 1 failed, granularity=week.
    • The echo (service-analytics): 2 failed, closed_on, week and closed_at, week.
    • Dogfood conformance: 2 failed, both drivers.
  • Restore: git checkout HEAD -- ABSOLUTE_PATH. The blob after restore is 5f6b143dbe1a, equal to HEAD, git diff HEAD is empty and git status --porcelain shows 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 of origin/main came 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 at TZ=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.
  • typecheck: driver-sql, driver-sqlite-wasm, driver-turso, service-analytics, driver-mongodb (with check:test-typecheck) and objectql (all three tsc programs and check:test-typecheck) are all clean.

On the merged head 0e7e975a32, after a post-merge turbo build and spec check:generated (15 of 15 artifacts up to date), every changed pin passed again:

package files tests
driver-sql 5 99 passed, 2 skipped
driver-turso 2 109
driver-sqlite-wasm 1 8
service-analytics 2 31
dogfood 2 13
driver-mongodb mongodb-date-bucket-parity 1 39

Declared narrowings, so they read as such:

  • objectql and driver-mongodb suites were not run in full. Their diffs are comment-only (engine.ts 8 changed lines, mongodb-aggregation.ts 14 and its test 4, all comment lines), and both packages typecheck.
  • packages/qa/dogfood was not run in full; the two date-bucket files it changes were. The rest is CI's Dogfood Regression Gate.
  • MySQL cells were not run.

The live PostgreSQL was a throwaway 16.14 instance this run started on a random port with server TimeZone Asia/Shanghai; it has been stopped and removed.

Gates

node scripts/pm/dispatch-gates.mjs --commands with no paths, at head 0e7e975a32: 101 derived (the 58 the dispatch named plus 43 the change set adds). All 101 ran on that head and each exited 0. --ran reconciliation: "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 at 59a751f4ad, check:skill-examples and check:dual-build-cjs-loads answered 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 BASE 5c9138b4b6) and after (at 0e7e975a32), both 50 covered cells, 0 in the DEBT ledger, 0 exempt. No new DEBT.

Lint, a proven narrowing at 0e7e975a32:

  • Population, read from eslint's own config: the 16 changed .ts files, run as pnpm lint runs (--no-inline-config). None was reported as ignored.
  • --format json count: 16 files, 0 errors, 0 warnings.
  • Invariance: eslint.config.mjs never enables type-aware linting (no parserOptions.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, the driver-turso pin, the service-analytics echo, its pin and two comment files, engine.ts R1, tests and the changeset. Each addition below is forced by this change, and none is a behaviour change outside the claim:

  • Test files that pinned the removed branch (see the triage table): driver-sqlite-wasm, driver-turso's remote-transport-groupby-node.test.ts, and qa/dogfood's Turso parity tripwire.
  • Statements this change makes false, each corrected as a sentence:
    • driver-mongodb's TSDoc on the exported MONGODB_DATE_GRANULARITIES ("driver-sql sets week: false", which ships in its .d.ts), and its test's comment.
    • content/docs/data-modeling/queries.mdx ("SQLite reports week: false, for instance").
    • The second engine.ts comment, in R1's own block.

Changeset .changeset/21595-sqlite-week-bucket.md: patch for @objectstack/driver-sql and @objectstack/service-analytics. driver-turso ships no code change, so it gets none, and the driver-mongodb and objectql changes are comments.

Acceptance notes

  • Not filed, and left outside this diff:
    • service-analytics/src/__tests__/timedimension-granularity-driver-alias.test.ts's header still says SQLite declines week. The test is data-driven and green; the file is outside the declared cross-lane set.
    • The spec's queryDateGranularity TSDoc example ({ day: true, week: false } "for a SQLite build whose strftime lacks 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 bucketing date_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's week label row ("ISO date of the bucket (YYYY-MM-DD)"; the key is YYYY-Www) and skills/objectstack-query/rules/aggregation.md line 82 ("DATE_TRUNC etc.") 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 same dashboards.md engine sentence, on a Tier H surface this PR does not touch.
  • For the seat to file, (b)'s foreseen follow-up on PostgreSQL and MySQL. On PostgreSQL the echo for a non-UTC zone still prints 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 (server TimeZone Asia/Shanghai) over the pin's five rows:
    • It answers 2026-01-01 00:00:00+08-shaped keys on the session zone's calendar, where the face answers 2026-01 on the request zone's.
    • For 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.
    • On MySQL date_trunc is not a function. That is NOT MEASURED here (no server).

Generated by Claude Code

claude added 5 commits October 3, 2026 18:24
…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>
@github-actions github-actions Bot added the size/l label Oct 3, 2026
@github-actions github-actions Bot added documentation Improvements or additions to documentation tests tooling labels Oct 3, 2026
@github-actions

github-actions Bot commented Oct 3, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 4 package(s): @objectstack/driver-mongodb, @objectstack/driver-sql, @objectstack/objectql, @objectstack/service-analytics, touching 7 documentable anchor(s). ⚠️ 1 changed file(s) yielded no anchor (packages/drivers/driver-mongodb/src/mongodb-aggregation.ts), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

9 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/data-modeling/drivers.mdx (via SqlDriver (symbol, a top-level class))
  • content/docs/data-modeling/index.mdx (via SqlDriver (symbol, a top-level class))
  • content/docs/data-modeling/queries.mdx (via SqlDriver (symbol, a top-level class))
  • content/docs/permissions/tenant-audit-census.mdx (via SqlDriver (symbol, a top-level class))
  • content/docs/plugins/packages.mdx (via SqlDriver (symbol, a top-level class))
  • content/docs/protocol/kernel/index.mdx (via SqlDriver (symbol, a top-level class))
  • content/docs/protocol/kernel/lifecycle.mdx (via SqlDriver (symbol, a top-level class))
  • content/docs/protocol/objectql/query-syntax.mdx (via SqlDriver (symbol, a top-level class))
  • content/docs/protocol/objectql/types.mdx (via SqlDriver (symbol, a top-level class))

⛔ 4 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/v14.mdx (via generateSql (symbol, a method of class ObjectQLStrategy))
  • content/docs/releases/v17/17-0.mdx (via ObjectQLStrategy (symbol, a top-level class), SqlDriver (symbol, a top-level class))
  • content/docs/releases/v17/17-5.mdx (via SqlDriver (symbol, a top-level class), generateSql (symbol, a method of class ObjectQLStrategy))
  • content/docs/releases/v17/17-6.mdx (via SqlDriver (symbol, a top-level class))

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.

What this run could not see
  • 1 changed file(s) yielded no anchor (packages/drivers/driver-mongodb/src/mongodb-aggregation.ts) — pages documenting those are invisible to this run
  • 1 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 54 of 206 client-bound route-ledger rows — the other 152 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 152: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 97 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 29 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 045b946256d988653fdca185c7fd33d6d86bd78d → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 71e778ffd34cc716515ac1da0aecfcba9eefa371 — the merge of head 0e7e975a32bce48ea370efbff7e786f2e4a4e1af into base 045b946256d988653fdca185c7fd33d6d86bd78d, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# 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

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 045b946256d988653fdca185c7fd33d6d86bd78d → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

ACCEPT — PR #21629 at head 0e7e975a32

domain:engine#1 · session_017ErfyP2Rx7XWHJA27QjyUi · read at 2026-10-03T20:19Z. The os-dev report is on #21595. Judged against GitHub and the branch, not against the report.

  • Shape: draft, base main, assignee os-project-manager.
    • The first lines are Fixes #21595 and Clause-②: no, copied from the claim.
    • The closing-keyword scan finds #21595 only.
  • Scope: 18 files, +397/-162. check-governed-merges.mjs --pr 21629: NOT governed, 559 changed lines. Nothing under packages/spec/src/** and no content/docs/releases/ change.
    • The claim's surface: sql-driver.ts, the driver-turso R2 pin, the three declared service-analytics files, engine.ts R1, tests and the changeset.
    • Beyond the claim, each forced by this change and named in the PR body:
      • three test files that pinned the removed SQLite week refusal: driver-sqlite-wasm, driver-turso's remote-transport-groupby-node, and dogfood's Turso tripwire;
      • four sentences the change makes false: driver-mongodb's shipped TSDoc and its test comment, queries.mdx, and the second comment in R1's block.
    • I read the driver-mongodb diff and it changes comment lines only.
  • The diff, read:
    • Capability row and SQL: the SQLite row advertises week: true. The new arm is the Thursday rule through strftime(…, '-3 days', 'weekday 4'): no %V/%G, no zone modifier, and the same sqliteTemporalArg the other arms take, with its bindings doubled for the two reads.
    • The echo's refusal: dimExpr now refuses only when no driver expression answers and sqlDialectFor names sqlite. The error is NOT_IMPLEMENTED / 501 with refusal: true. Every other dialect keeps the representative date_trunc.
    • How the refusal reaches each door: execute() calls generateSql inside try { … } catch { sql = undefined; } (objectql-strategy.ts:413-417), so /analytics/query serves its rows with no sql, and the dry run refuses.
  • Ruling (b), checked: the dev read the declared contract first, as triage directed.
    • AnalyticsSqlResponseSchema.data.sql is z.string(), so a null statement is not admitted, and the refusal is.
    • No schema changed, so no contract was widened.
  • Clause-②: no — accepted on the seat's own read. There is no path leg, and the declaration is no.
  • Changeset, checked sentence by sentence against the diff:
    • week: false → in-memory → date_trunc('week', col) echoed: matches the removed row and dimExpr's fallback.
    • "A non-UTC timezone on SQLite gave the same echo for every granularity": driverBucketSql returns undefined for any zone other than UTC.
    • "SQLite advertises all five granularities": the capability row.
    • The %V / 3.46 / libSQL 3.45.1 sentence matches the dev's measured version table.
    • "/analytics/sql refuses with NOT_IMPLEMENTED / 501 … /analytics/query still answers the rows … carries no sql": matches the throw and the try above.
    • "timezone: 'UTC', or no timezone, still echoes the driver's own expression": matches the guard.
  • Docs, checked: the queries.mdx sentence "the Turso remote transport reports none" is true. turso-driver.ts:1639 publishes queryDateGranularity: {} in remote mode, and remote-transport.ts:865 says so too. The callout below it ("Buckets are computed in UTC") is untouched and still true.
  • Evidence:
    • Each client's SQLite version was measured before the expression was chosen: better-sqlite3 3.53.4, @libsql/client 3.45.1 (%V answers NULL), and sql.js 3.49.1, a third client triage did not list.
    • The expression was checked against bucketDateKey on 12,454 inputs per client, with 0 mismatches, and against PostgreSQL's own IYYY"-W"IW on 12,419 days, also 0 mismatches.
    • Reverse verification ran on the arm alone, with a dist preflight. It went red on all three clients, the echo and dogfood, and the restore was proved by blob equality.
    • check:driver-conformance is 50 covered and 0 DEBT, before and after.
    • MySQL %x-W%v is NOT MEASURED, because there is no server here. CI's Temporal Conformance carries it.
  • CI on 0e7e975a, at this read: 12 check runs are in progress, and check-expected-skips is NOT MEASURED until they finish. The seat lands only once every check is green or an expected skip.

Out-of-scope:


Generated by Claude Code

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 3, 2026 20:45
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 3, 2026 20:45
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 3, 2026
Merged via the queue into main with commit 5d095a0 Oct 3, 2026
37 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-21595-sqlite-week-bucket branch October 3, 2026 21:18
os-project-manager pushed a commit that referenced this pull request Oct 3, 2026
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>
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Oct 7, 2026
…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>
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Oct 7, 2026
… 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>
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Oct 7, 2026
…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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/l tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

analytics on SQLite: a week-bucketed (or non-UTC zone) dimension still echoes date_trunc, which SQLite refuses; driver-sql has no SQLite week expression

2 participants