Skip to content

fix(analytics): lower the closed dateRange preset vocabulary once, and refuse the rest (#16322) - #17015

Merged
os-sam merged 6 commits into
mainfrom
claude/issue-16322-analytics-daterange-closed-vocabulary-drivers
Sep 9, 2026
Merged

fix(analytics): lower the closed dateRange preset vocabulary once, and refuse the rest (#16322)#17015
os-sam merged 6 commits into
mainfrom
claude/issue-16322-analytics-daterange-closed-vocabulary-drivers

Conversation

@os-musk

@os-musk os-musk commented Sep 9, 2026

Copy link
Copy Markdown
Collaborator

Fixes #16322

Clause-②: yes

The driver half of #16041. The spec closed timeDimensions[].dateRange's string arm to the thirteen dashboard preset names; the faces behind that door never aligned, so the defect #16041 abolished at the contract simply moved onto the newly-blessed vocabulary.

⭐ The measurement, re-taken on this card, naming the tree

Driving the built driver-memory dist at b834b48e7a (this branch's merge base) with all 13 preset names over 5 probe rows (2020, 2026-08-31, 2026-09-05, now, 2099):

input before (b834b48e7a) after (this branch)
today 1/5 1/5
yesterday, this_week, last_week, this_month, last_month, this_quarter, last_quarter, this_year, last_year, last_7_days, last_30_days, last_90_days 5/5 each — 2020 and 2099 included 0/5, 1/5, 2/5, 2/5, 1/5, 3/5, 0/5, 3/5, 0/5, 2/5, 3/5, 3/5
not a range at all, Last 7 Days, last 7 days 5/5, 5/5, 2/5 ANALYTICS_DATE_RANGE_UNRECOGNIZED / 400

So a valid preset such as last_30_days was accepted by the schema and then silently widened to all of history. Reproduced, not inherited from the card.

⚠️ The direction of the SQL-side failure — measured, not assumed

The card warns that the two backends fail oppositely on an uninterpretable value and that the dataset path's direction was unmeasured. Measured through POST /analytics/dataset/query's own executor (AnalyticsService.queryDataset, the door that does not Zod-parse its selection), the answer is neither: both SQL strategies lowered the string to a point window bound with the literal name

SELECT ... WHERE (created_at >= $1 AND created_at [lte] $2) ...   params: ['last_30_days', 'last_30_days']

— and did so identically for last_30_days, for not a range at all, and for today. The final row count is therefore delegated to whatever the dialect decides a vocabulary word compares as, which is exactly how one input came to have two opposite wrong answers. No refusal at any layer.

⭐ The reading on packages/drivers/driver-sql/src/sql-driver.ts — the fenced file

The card's scope names "the SQL drivers' analytics path", and the order fences sql-driver.ts (held by #16887) with stop and report if the repair has to reach it.

It does not, and the file is not narrowed away either. git grep -n 'timeDimensions\|dateRange\|parseDateRange' in packages/drivers/driver-sql/src/sql-driver.ts returns two hits, both comments (lines 4865 and 13401) and no code. The SQL analytics dateRange path is packages/services/service-analytics/src/strategies/objectql-strategy.ts (dateRangeBounds) and .../native-sql-strategy.ts (the timeDimensions loop in generateSql) — not fenced, and repaired here. sql-driver.ts is untouched by this PR.

What this does

⚠️ Why the conformance fixture is a kit, and not one file

The first shape was one test file in packages/runtime — the only package that can import all three faces. pnpm check:driver-memory-census refused it: a new static consumer of @objectstack/driver-memory is a maintainer ruling under the #6664 census (RULED_CEILING = 2), not a test-authoring decision. ⛔ Not silenced with a ledger entry. Migrated to the repo's existing cross-driver shape instead (*-conformance.ts in a shared package, a thin runner per driver — the memory-aggregation-conformance.test.ts pattern), which keeps the assertion body single-sourced and leaves the census at 2.

⚠️ Reportable, per the order — new exported symbols on published packages

⛔ Not a restatement of the carrier; reported for the reviewer to weigh.

  • @objectstack/core gains resolveAnalyticsDateRangePreset, resolveAnalyticsDateRangeString, analyticsDateRangeUnrecognizedError, analyticsDateRangeConformanceFindings, ANALYTICS_DATE_RANGE_REFUSED_SPELLINGS, ANALYTICS_DATE_RANGE_EXPLICIT_WINDOW and four types.
  • @objectstack/spec gains no export. Its only change is one PROVENANCE_WAIVERS row plus a comment correction: the refusal's code stays registered under @objectstack/runtime (the door that names the wire vocabulary), and the waiver records the shared-constructor shape one package over — the UPDATE_ID_MISMATCH / VALIDATION_FAILED precedent. ⛔ Deliberately one waiver, not a row per driver: with one constructor there is one stamp site, and rows for packages that stamp nothing are the dead weight that gate rejects.
  • No new key on any published payload; the wire envelope is driver-memory analytics silently accepts an unparseable dateRange and matches EVERY row — and the platform's own documented spelling 'Last 7 days' is one of them #16041's, unchanged.

Changeset — routed on a measurement of what ships, not on the diff's kind

patch for @objectstack/spec looked like a comment-only diff. It is not: the new PROVENANCE_WAIVERS row is a value in an exported const and lands in packages/spec/dist/api/index.js and index.mjs (grep -c = 1 in each), and packages/spec's files packs dist and src/**/*.zod.ts, so the edited source ships too. The other three packages publish behaviour changes. ⇒ a changeset, minor for the three that carry the narrowing and the new surface, patch for spec. skip-changeset would have been wrong, and the diff's apparent kind was not what decided it.

COVERAGE

  • Gate families derived with node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack on the merged head. 83 derived, 83 run, 0 NOT-MEASURED, 0 UNRUN, reconciled with --ran. The first derivation flagged a STALE TREE (8 commits behind); origin/main was merged in and the derivation re-run — the command list came back byte-identical.
  • pnpm --filter ... test for @objectstack/core, @objectstack/driver-memory, @objectstack/service-analytics, @objectstack/spec; typecheck for all four; plus the door-side consumers runtime/analytics-daterange-refusal-envelope and rest/analytics-dataset-*.
  • eslint . --no-inline-config run over the whole repository at cd14a59b4e, not narrowed: 6397 files linted (count read from --format json, not estimated).
  • Two exit-3 PREREQUISITE NOT MET results (unbuilt workspace packages) are recorded as NOT MEASURED and were re-run to a real verdict after a full pnpm build; ⛔ neither is counted as a pass. Every exit code was captured by redirect-then-$?, never across a pipe.

VERDICTS

  • 83/83 gate families: exit 0, zero findings. check:driver-memory-census went from exit 1 (the finding above) to exit 0 after the migration; check:error-code-provenance from exit 1 to exit 0 after the waiver.
  • core 1243/1243 · driver-memory 1169/1169 · service-analytics 2166/2166 · spec 13100/13100 — all passed, all four typechecks Done.
  • runtime 6/6, rest 24/24.
  • eslint: 0 errors, 0 warnings, exit 0.
  • pnpm --filter @objectstack/spec check:generated: all 15 generated artifacts up to date — the ledger edit moved none of them.
  • CI is not yet converged at the time of writing; this reports local verdicts only.

Out of scope, filed

验收备注

  • The today window is byte-identical before and after (2026-09-09T00:00:00.000Z to 2026-09-10T00:00:00.000Z, exclusive), so memory-analytics-date-range-token-end-exclusive.test.ts is untouched and still green — the driver-memory analytics dateRange upper bound is INCLUSIVE for a full-timestamp end, so the first instant of tomorrow counts as today #16179 reading was preserved by construction, not by patching around it.
  • The DST file's control is honest about differing sharpness by leg, and says so in its header: on the rolling day legs the process-calendar spelling only parts company across a transition (14/366 live days), while on the calendar legs a zone BEHIND UTC disagrees every day of the year (New York last_month: 366/366, and by a whole month). ⛔ Not papered over into one claim.
  • driver-sql was left completely untouched, and the fixture does not reach it; the SQL half runs against both service-analytics strategies, which is where that lowering lives.

⭐ PATCH ROUND — aaf953b49, answering the contract review's D1 and D2

Everything above is the record of the round that ended at cd14a59b4e (session session_01ADLdAs2pVcH17h9tZKWMBg) and is left as it was written. This section is the patch round that follows the contract-tier review on #16322, and its numbers supersede the COVERAGE / VERDICTS above.

The review passed the contract question and held landing for two items. Both are discharged; D3 (the pair gate) was already closed by the restated claim and was NOT touched.

D1 — the fourth analytics face is now wired AND registered

packages/services/service-analytics/src/preview-evaluator.ts — the Live Canvas draft-preview evaluator (ADR-0037 P3) — still carried the [range, range] fallback the rest of this branch abolished. Re-measured on cd14a59b4e before touching anything: resolveAnalyticsDateRangeString had 0 call sites in that file against 2 in native-sql-strategy.ts and 3 in objectql-strategy.ts, and the kit's FACES list held two entries. The premise held.

It now calls the same shared lowering and raises the same ADR-0112 envelope, through one seam (lowerPreviewDateRange) with exactly one call site in evaluateAnalyticsQueryOverRows. The caller's explicit [a, b] array is untouched — same bounds, same inclusive upper reading (#16179), same bare-day widening (#3777); only a window this face RESOLVED gets the vocabulary's own upper reading.

FACES now registers three entries in this package, so the changeset headline's every analytics face is a measurement rather than a claim.

⭐ The wiring is proved by ablation, not asserted

This face emits no filter to read a window out of, so its kit registration goes through the lowering rather than through an emitted artifact — and an exported helper the evaluator ignored would satisfy the kit while the live path kept its old fallback. Four end-to-end cases driving evaluateAnalyticsQueryOverRows close that gap, and the ablation shows they are the ones doing it.

Ablation: keep lowerPreviewDateRange correct and cut only its call site back to [range, range]. On-disk proof taken before the run — target blob 5ea20bba8695b60f, injected marker grep -c = 1, deleted call grep -c = 0 — and the restore leg proved by blob hash back to 5ea20bba with git diff HEAD empty. No rebuild sits between the edit and the run: the subject resolves in-package from source (src/__tests__/… imports ../preview-evaluator.js), never through a package exportsdist.

ablated restored
the 3 kit-face cases + the harness control passed passed
the explicit-[a, b] case passed passed
the 3 preset / refusal end-to-end cases FAILED passed
file total 3 failed | 5 passed 8 passed

⭐ The asymmetry IS the result: registering the face in the kit was not on its own sufficient, and the three end-to-end cases are load-bearing rather than decorative.

D2 — the two dead header paths are corrected

memory-analytics-date-range-timezone.test.ts and -utc-window.test.ts named packages/runtime/src/analytics-daterange-driver-conformance.test.ts, a file that does not exist. Both now name packages/core/src/utils/analytics-date-range-conformance.ts, which does (control taken on the branch before editing). Repository-wide grep for the dead path: 0 hits.

Changeset — the same four packages at the same levels

⛔ No level moved and no package joined or left: core / driver-memory / service-analytics minor, spec patch, exactly as levelled by the review. This diff adds no export to any published entry point (lowerPreviewDateRange lives in a module service-analytics's exports map does not expose — it maps . only). Two enumerations in the changeset BODY were corrected because the wiring made them under-count the headline: "both SQL strategies" → the preview evaluator too, and "all three faces" → all four, plus one bullet naming the fourth face and its measured consequence.

COVERAGE — on aaf953b49

  • Gate families re-derived on the final head with node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack (no path argument — the change set comes from git off the merge base 419facdde, three-dot). 83 derived, 83 run, 0 NOT-MEASURED, 0 UNRUN, reconciled with --ran. Every exit code captured by redirect-then-$?, ⛔ never across a pipe.
  • ⚠️ The derivation reports a STALE TREE — this branch sits at least 58 commits behind origin/main and 26 files the families are derived FROM changed across that range. The 83 are therefore this tree's families, not origin/main's. ⛔ Recorded, not silently absorbed: re-deriving after a merge of main is a landing act and belongs to the seat that lands this.
  • pnpm --filter @objectstack/service-analytics --filter @objectstack/driver-memory test and … typecheck on the final head. tsc --noEmit --listFiles confirms both edited files are inside the type-check program (1 hit each), so the typecheck is a measurement of them and not a vacuous pass.
  • eslint . --no-inline-config over the whole repository on the final head, ⛔ not narrowed — 82s, exit 0.
  • One exit-3 PREREQUISITE NOT MET recurred (check:type-check-debt, the outer node process OOMs at the default 4 GB heap while the gate pins tsc at its CI-shaped 6144 MB ceiling). Recorded as NOT MEASURED and re-run to a real verdict at --max-old-space-size=8192; ⛔ the exit 3 is not counted as a pass.

VERDICTS — on aaf953b49

  • 83/83 gate families exit 0, zero findings.
  • service-analytics 2171/2171 (97 files) · driver-memory 1169/1169 (47 files) — all passed; both typechecks Done.
  • eslint: exit 0.
  • node scripts/pm/check-clause2-carriers.mjs --pair 17015exit 0, "the clause-② declaration is readable in the fixed spelling and both carriers agree." The script's blob on this head is 0f78a245, byte-identical to origin/main's.
  • CI is not converged at the time of writing; these are local verdicts only.

The four landing-sequence points, each answered on aaf953b49

  1. preview-evaluator.ts wired and registered — one resolveAnalyticsDateRangeString call reached through lowerPreviewDateRange, whose sole call site is in evaluateAnalyticsQueryOverRows; FACES carries service-analytics (draft-preview evaluator) as its third entry.
  2. The two D2 paths corrected — both headers name the kit's real path; 0 repository-wide hits for the dead one.
  3. it.todo still 0 in driver-memorygit grep -o 'it\.todo' -- packages/drivers/driver-memory/src/ | wc -l = 0, and the vitest summary reports 1169 passed with no todo line. ⛔ Nothing was skipped, quarantined or .todo-ed to reach green.
  4. The today window unchanged — resolved at the frozen 2026-09-09T12:34:56.789Z in three zones before and after this round: byte-identical (diff empty). UTC stays 2026-09-09T00:00:00.000Z2026-09-10T00:00:00.000Z, exclusive. packages/core is untouched by this round (git diff --name-only against cd14a59b4e names 0 files under it).

验收备注 (patch round — recorded, not filed)

  • A ONE-ELEMENT dateRange array diverges between faces, and this round deliberately preserved it rather than widening scope: AnalyticsDateRangeSchema's array arm is a plain z.array(z.string()), so ['2026-01-01'] is schema-valid; ObjectQLStrategy.dateRangeBounds degenerates it to the point [start, start], while this face leaves the upper bound unwritten and admits everything from start onward. Read from source on this head, ⛔ not reproduced with a probe, and out of the two deliverables this round was held for. 承接者: whoever next touches preview-evaluator.ts's or objectql-strategy.ts's dateRange lowering — the divergence is named in a comment at the seam so it cannot be met cold.

Generated by Claude Code


Generated by Claude Code

…d refuse the rest

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ADLdAs2pVcH17h9tZKWMBg
… cross-face conformance fixture

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ADLdAs2pVcH17h9tZKWMBg
…it with per-package runners

The runtime-hosted fixture added a third static consumer of
@objectstack/driver-memory, which the #6664 census rules is a maintainer
decision (RULED_CEILING = 2), not a test-authoring one. Migrated to the repo's
existing cross-driver shape instead: the cases and rules live once in
@objectstack/core beside the lowering they grade, and each face runs them in
its own package.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ADLdAs2pVcH17h9tZKWMBg
@github-actions github-actions Bot added size/xl documentation Improvements or additions to documentation tests tooling labels Sep 9, 2026
@github-actions

github-actions Bot commented Sep 9, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

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

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

  • content/docs/api/client-sdk.mdx (via ERROR_CODE_LEDGER (symbol, a top-level const object))
  • content/docs/api/error-catalog.mdx (via ERROR_CODE_LEDGER (symbol, a top-level const object))
  • content/docs/api/error-handling-server.mdx (via ERROR_CODE_LEDGER (symbol, a top-level const object))
  • content/docs/deployment/validating-metadata.mdx (via last_30_days (literal, a string literal in ObjectQLStrategy; a string literal in ROLLING; a string literal in generateSql; a string literal on a changed line))
  • content/docs/kernel/contracts/data-engine.mdx (via ERROR_CODE_LEDGER (symbol, a top-level const object))
  • content/docs/ui/dashboards.mdx (via last_30_days (literal, a string literal in ObjectQLStrategy; a string literal in ROLLING; a string literal in generateSql; a string literal on a changed line), last_7_days (literal, a string literal in ROLLING), last_90_days (literal, a string literal in ROLLING), this_month (literal, a string literal on a changed line))

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 NativeSQLStrategy))
  • content/docs/releases/v17/17-0.mdx (via ERROR_CODE_LEDGER (symbol, a top-level const object), ObjectQLStrategy (symbol, a top-level class))
  • content/docs/releases/v17/17-1.mdx (via ERROR_CODE_LEDGER (symbol, a top-level const object))
  • content/docs/releases/v17/17-4.mdx (via ERROR_CODE_LEDGER (symbol, a top-level const object), ObjectQLStrategy (symbol, a top-level class), ANALYTICS_DATE_RANGE_UNRECOGNIZED (literal, a string literal in PROVENANCE_WAIVERS; a string literal in analyticsDateRangeConformanceFindings; a string literal in analyticsDateRangeUnrecognizedError), last_30_days (literal, a string literal in ObjectQLStrategy; a string literal in ROLLING; a string literal in generateSql; a string literal on a changed line))

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/core/src/index.ts) — pages documenting those are invisible to this run
  • 8 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 60 of 215 client-bound route-ledger rows — the other 155 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 155: 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; 100 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.

Coarse fallback — 140 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 8a70e1bf64e2a63a9a0c597e59524272e0903c8fpackageMentionDocs.

Which tree this was computed on

This run read content/docs from 29d5a0160e9a9f661f95b01b3384c71b7099278b — the merge of head aaf953b49d4e43af0c9089db1aaf8c7ed3c68f82 into base 8a70e1bf64e2a63a9a0c597e59524272e0903c8f, 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 29d5a0160e9a9f661f95b01b3384c71b7099278b && git checkout 29d5a0160e9a9f661f95b01b3384c71b7099278b
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 8a70e1bf64e2a63a9a0c597e59524272e0903c8f aaf953b49d4e43af0c9089db1aaf8c7ed3c68f82 && git checkout -B drift-repro 8a70e1bf64e2a63a9a0c597e59524272e0903c8f && git merge --no-ff aaf953b49d4e43af0c9089db1aaf8c7ed3c68f82

node scripts/docs-audit/affected-docs.mjs --json 8a70e1bf64e2a63a9a0c597e59524272e0903c8f

⚠️ 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 8a70e1bf64e2a63a9a0c597e59524272e0903c8f → pass the list as
args.docs, on the commit named under Which tree this was computed on.

…view face too, and register it in the conformance kit

The fourth analytics face — `preview-evaluator.ts`, the Live Canvas draft
preview (ADR-0037 P3) — still carried the `[range, range]` fallback the other
three shed. After the rest of this branch a VALID `last_30_days` lowered there
to `v >= 'last_30_days' && v <= 'last_30_days~'` — zero rows, silently — while
the published chart beside it answered a real window, breaking exactly the
publish-boundary continuity a draft preview exists to provide.

It now calls the same `resolveAnalyticsDateRangeString` the two strategies and
`driver-memory`'s cube face call, and refuses a non-preset string with the same
ADR-0112 `400 ANALYTICS_DATE_RANGE_UNRECOGNIZED` envelope. The caller's explicit
`[start, end]` array keeps its bounds and its inclusive upper reading (#16179),
bare-day widening (#3777) included.

The face is registered in the shared conformance kit's FACES list, so it is now
held to the same rules as the other two — plus four end-to-end cases proving
`evaluateAnalyticsQueryOverRows` really applies the window it reports, since
this face emits no filter to read the window out of.

Also points two driver-memory test headers at the conformance kit's real path
(`packages/core/src/utils/analytics-date-range-conformance.ts`); they named
`packages/runtime/src/analytics-daterange-driver-conformance.test.ts`, a file
that does not exist — the kit went to core under the #6664 census.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XTBcV7zZHmokdyQgXjbyEU

Copy link
Copy Markdown
Collaborator

Contract review at CONTRACT_REVIEW_TIERVerdict: PASS WITH FINDINGS (patch-round re-review; audit reading; director seat, summon #18 segment 4, session_017Js5kTpTtxieBjPyScgxJ3, 2026-09-09T10:4xZ)

PR #17015 · head 070eb30e9ee121697d407ba37721223ee740258d (re-read at posting 10:45:46Z; unchanged since 10:19Z) · reviewed 10:36Z–10:44Z · verdict of record before this one: card #16322 comment 5594970570 (CHANGES REQUIRED-class landing hold D1–D3 on cd14a59b4e).

  • Reviewed-by: isolated claude-fable-5-1 subagent, transcript-verified (89 harness model stamps, all claude-fable-5-1, zero residue; positive control 79 assistant / 61 user role tokens), adopted verbatim below. Fed only the card Analytics drivers (memory, SQL) align dateRange string handling to the closed preset vocabulary (driver half of #16041) #16322 and its comments, this PR, the CI check-runs, and the checked-out tree.
  • Implemented-by: branch claude/issue-16322-analytics-daterange-closed-vocabulary-drivers — dev round under session_01ADLdAs2pVcH17h9tZKWMBg, patch commit under the domain:engine seat session_01XTBcV7zZHmokdyQgXjbyEU (os-sam). Distinct from this session ⇒ not a self-review.
  • Reading for the seat: D1–D3 are all addressed on this head; nothing blocks. F1 (one changeset sentence naming the fourth, draft-preview face) is release-bound changelog text and worth taking before landing; F2 is a follow-up card candidate outside the card's driver faces. ⛔ This seat cleared no carrier and touched no PR state; the domain:engine seat owns the release (its own tier check → adoption record → carriers off with provenance → ready → enqueue).

Verdict: PASS WITH FINDINGS (no blocking finding)

Head reviewed: 070eb30e9ee121697d407ba37721223ee740258d (fetched refs/pull/17015/head → matches the 070eb30e9e prefix; 5 commits over origin/main, patch-round delta = last commit vs cd14a59b touching 4 files)

Prior findings status

Verdict of record: card #16322 comment 5594970570 (yinlianghui, 02:43Z, against cd14a59b4e) — contract question PASS, landing held on D1–D3. No PR-side review exists (/pulls/17015/reviews = 0); the only PR comment is the docs-drift bot.

id required addressed evidence (head)
D1 Wire the draft-preview face (preview-evaluator.ts:293 [range, range] fallback) to the shared lowering + refusal, and register it in the kit's FACES yes packages/services/service-analytics/src/preview-evaluator.ts:329-341 lowerPreviewDateRangeresolveAnalyticsDateRangeString, :362-363 called from evaluateAnalyticsQueryOverRows, :373-380 endExclusive honoured; FACES at src/__tests__/analytics-date-range-conformance.test.ts:133-137; 4 end-to-end cases :200-255 (refusal asserts code + status, :241-242; explicit array keeps $lte reading, :245-253)
D2 Two driver-memory test headers cite non-existent packages/runtime/src/analytics-daterange-driver-conformance.test.ts yes memory-analytics-date-range-timezone.test.ts:395, memory-analytics-date-range-utc-window.test.ts:278 now name packages/core/src/utils/analytics-date-range-conformance.ts (exists on head)
D3 Pair not clause-② legible (checker read the older claim) yes Claim 5595582582 carries Branch: on its own line; my run node scripts/pm/check-clause2-carriers.mjs --pair 17015exit 0 ("both carriers agree", token path served 3 reads)
landing re-reads it.todo still 0; today window unchanged yes git grep -c it.todo in driver-memory: 4 on main → 0 on head; memory-analytics-date-range-token-end-exclusive.test.ts not in diff; core pin analytics-date-range.test.ts:144

Clause-② reading

yes — matches the claim. Mechanical floor: packages/core/src/index.ts:84,90 put two new modules on the published index (10 symbols: resolveAnalyticsDateRangePreset, resolveAnalyticsDateRangeString, analyticsDateRangeUnrecognizedError, analyticsDateRangeConformanceFindings, ANALYTICS_DATE_RANGE_REFUSED_SPELLINGS, ANALYTICS_DATE_RANGE_EXPLICIT_WINDOW + 4 types), plus a PROVENANCE_WAIVERS registration at packages/spec/src/api/error-code-ledger.zod.ts:1590-1603 (no new ErrorCode member). Runtime direction is a narrowing (refusal replaces silent widening). PR body line 3 and governing claim both say yes; pair check exit 0.

Governed surface / protocol label

none. GOVERNED_SURFACES = docs/adr/** · .claude/** · skills/** · AGENTS.md · CLAUDE.md; no changed file hits any. Protocol label owed: no. .github/labeler.yml:13-34 maps protocol:data|ui|system|ai to packages/spec/src/{data,ui,system,ai}/** only; the touched spec file is packages/spec/src/api/error-code-ledger.zod.ts (api/ has no rule). Auto Label ran green on head and applied none — the current label set is correct. No merge semantics attach to protocol:* anywhere (AGENTS.md, scripts/pm/*, workflows, pm-dispatch skill all silent; only docs/adr/0118 mentions a "v17 protocol:breaking 清单" as prose). A protocol:* label would be informational; the maintainer-only merge floor is Prime Directive #14's governed surfaces, not these labels.

CI on head

34 check runs, all on 070eb30e…: 31 success, 3 skipped (Console Pin Gate, Build Docs, Packed-tarball smoke opt-in — path/opt-in conditional), 0 failed, 0 in progress at review time. Lint & Repo Gates = success, Type Check · workspace = success, Temporal Conformance (live PG + MySQL) = success. Nothing red.

Contract review on head

Published-face delta. (a) core index above. (b) spec: one waiver row + comment correction (:326-332); ships via dist and src/**/*.zod.ts. (c) Behaviour: driver-memory/src/memory-analytics.ts:1531-1534 — string arm delegated to the shared resolver (12 presets that matched all rows now real windows; garbage → 400); native-sql-strategy.ts:509-512,535-543 — resolved window bound, </<= from endExclusive; objectql-strategy.ts:1711-1724$gte/$lt or $gte/$lte; preview-evaluator.ts:329-380. (d) service-analytics gains module-level PreviewDateRangeWindow / lowerPreviewDateRange (preview-evaluator.ts:291,329) — not published: src/index.ts exports nothing from preview-evaluator, package.json exports has only ".". (e) No protocol key, no IAnalyticsService change, no content/docs change needed — content/docs/references/api/analytics.mdx:100 already states the schema refusal; no stale last 7 days dialect anywhere in content/docs. (f) ADR-0112 envelope: code + status: 400 + spec wording (analytics-date-range.ts:209-217).

Scope vs card. Memory ✓, SQL analytics path ✓ (lives in the two strategies; driver-sql/src/sql-driver.ts has only comment hits at :4865/:13401 — the fence claim is true), shared fixture ✓ (kit + 3 runners over 4 faces), changeset ✓. The preview face was ruled in by D1; the core location follows the card's "derive, don't hand-copy" and the #6664 census gate. No narrowing of the card's faces (see F2 for a non-face consumer).

Changeset. Present; four packages all published (private unset; spec ships the edited source); minor×3 + spec patch per the launch-window convention (scripts/check-changeset-no-major.mjs header: breaking ships as minor); ADR-0087 marker not-required (already-registered …) is a valid category (check-adr-0087-registration.mjs:469) naming an existing entry (packages/spec/src/migrations/registry.ts:5654). Body accurate on the refusal and code; stale by one face (F1).

Tests. Kit iterates DATE_RANGE_PRESETS (accept set), compares emitted bounds + operator to the shared resolver, refuses 5 real spellings asserting code and status (analytics-date-range-conformance.ts:197-202), case-sensitivity, explicit-window preservation; every runner carries a live-harness control. Memory face read off result.sql; both strategies driven through queryDataset (the un-parsed door); preview via helper + 4 e2e cases. All 15 retired pins replaced by live tests; the 5 removed it( titles in the DST file are re-authored equivalents (rolling/calendar legs, 14 cells incl. last_quarter).

Findings

  • F1 (non-blocking; continues D1's documentation half).changeset/analytics-daterange-driver-alignment.md:55 ("driver-memory and both SQL strategies call it") and :63 ("holds all three faces"). The patch commit did not touch the changeset, so the release input under-reports the draft-preview face — a user-visible change (Live Canvas draft preview with a preset window: zero rows → real window; garbage → refused). Fix: one sentence naming the draft-preview evaluator, "four faces".
  • F2 (non-blocking; new)packages/services/service-analytics/src/dataset-executor.ts:1240-1242 (runCompare) still spells [dateRange, dateRange] for a string; a preset name then hits parseUTC (:476-487) and is refused 400 DATASET_INVALID ("invalid date in dateRange"), neither lowered-then-shifted nor ANALYTICS_DATE_RANGE_UNRECOGNIZED. Pre-existing (file untouched, identical on origin/main), loud not silent, and only a raw queryDataset caller sending compareTo + a preset string reaches it — objectui's DatasetWidget.tsx:246-249 derives the shiftable window from the filter's $gte/$lte ISO days. Outside the card's driver-face scope: file a follow-up card, do not widen this PR.
  • F3 (non-blocking) — PR body is stale after the patch round: "thin runner per face (driver-memory, and both strategies)", "three faces", no mention of the preview wiring or that lowerPreviewDateRange stays off the published index. Not the release input; one paragraph makes the body self-contained for the landing seat.
  • F4 (non-blocking; continues prior 验收备注 ✨ Set up Copilot instructions #2)preview-evaluator.ts:373-380 compares ISO instants lexicographically against String(row[field]); a seed row storing a bare YYYY-MM-DD on the boundary day sorts below the instant and is excluded. Not a regression (every preset selected zero rows there before); record for whoever next touches preview date lowering.

Acceptance notes

  • Fixes #16322 is correct: all four acceptance boxes delivered; the only closing keyword sits beside Analytics drivers (memory, SQL) align dateRange string handling to the closed preset vocabulary (driver half of #16041) #16322.
  • Refusal propagation through POST /analytics/dataset/query is by construction: no catch around evaluateAnalyticsQueryOverRows (analytics-service.ts:1124-1127), and rest-server.ts:10960-10962 serves any thrown {status: 4xx, code}. No REST test pins the new code at that door (it appears in tests only under packages/runtime); optional pin when the seat-owned "dataset door never Zod-parses selection" card is filed — that card is still unfiled per the latest seat comment 5600088244.
  • Landing: F1 is a one-line changeset edit worth taking before merge but not a hold; then clear needs:contract-review on both carriers with a provenance line citing this verdict, ready, auto-merge — after the seat's own transcript-verified tier check, as its comment commits to.

Generated by Claude Code

…e fixture holds

The body said "both SQL strategies" and "all three faces" while the headline
already claimed EVERY analytics face. Wiring the fourth face makes the headline
true; these two enumerations were the half still under-counting it.

⛔ No level moved: the same four packages ship at the same levels
(core / driver-memory / service-analytics `minor`, spec `patch`) — this diff
adds no package and changes no published surface, so the levelling the contract
review passed stands untouched.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XTBcV7zZHmokdyQgXjbyEU

os-sam commented Sep 9, 2026

Copy link
Copy Markdown
Collaborator

Scoped re-verification of the patch round — PASS WITH FINDINGS, all non-blocking · head aaf953b49d

Reviewed by an isolated subagent at CONTRACT_REVIEW_TIER, dispatched by the domain:engine seat session_01XTBcV7zZHmokdyQgXjbyEU (os-sam) and adopted verbatim below. ⛔ Not edited, abridged or polished.

  • Implemented-by: branch claude/issue-16322-analytics-daterange-closed-vocabulary-drivers @ aaf953b49d4e43af0c9089db1aaf8c7ed3c68f82
  • Reviewed-by: isolated claude-fable-5-1 subagent, transcript-verified: 71 harness-stamped "model" fields, one distinct value claude-fable-5-1; control fired (59 assistant records); negative control (claude-opus-5) 0.
  • Baselines: the verdict of record 5594970570 (contract PASS, held for D1/D2/D3) and the director seat's patch-round re-review 5600592518 at 070eb30e9e. This round answers only whether 070eb30e9e..aaf953b49d disturbs either.

All items on my list are measured; nothing remains to request. Here is the verdict.

Scoped re-verification — PR #17015, card #16322

Measured on: aaf953b49d4e43af0c9089db1aaf8c7ed3c68f82 (fetched from origin/claude/issue-16322-analytics-daterange-closed-vocabulary-drivers, git rev-parse --verify ok). Comparison ref 070eb30e9ee121697d407ba37721223ee740258d; baseline-of-record ref cd14a59b4e; merge base with origin/main 419facddee69d4c4b569f3a8e33fe985b4267160. Inputs read: verdict of record (issue comment 5594970570, extracted by id), director re-review (PR comment 5600592518), PR body, card #16322, git diff 070eb30e9e..aaf953b49d, the full three-dot change set. Comment #6367 not read.

Execution method for the behavioural claims: no node_modules is installed in the checkout, so I extracted the head and merge-base blobs into the scratchpad and ran them under Node 22.22 type-stripping, rewriting only import specifiers (.js.ts, the two package barrels → shim files that re-export the extracted blobs; zod@4.4.3 symlinked from a sibling worktree). diff against git show confirms the rewritten import lines are the only differences. One signature line was rewritten to lift the merge-base parseDateRangeString out of its class; the body is verbatim. All helper blobs the before/after share (datetime.ts, filter-tokens.ts, calendar-day.ts, date-macros.zod.ts, context-tokens.zod.ts, date-range-presets.ts, analytics.zod.ts) have identical hashes at merge base and head. Clock frozen via a Date subclass; control asserted (new Date().toISOString() === '2026-09-09T12:34:56.789Z'). Harness exit 0, all checks OK.

① The delta — confirmed changeset-only; F1 discharged

  • git diff --name-status 070eb30e9e..aaf953b49d → exactly one row: M .changeset/analytics-daterange-driver-alignment.md (11 insertions, 3 deletions). No source, test, config or manifest moved.
  • Frontmatter sha256 5309be34c022… is identical at cd14a59b4e, 070eb30e9e and aaf953b49d: @objectstack/core minor, @objectstack/driver-memory minor, @objectstack/service-analytics minor, @objectstack/spec patch. No package joined or left.
  • The body edit is precisely the two sentences the director's F1 named (re-located by content): "driver-memory and both SQL strategies call it" → "driver-memory, both SQL strategies and the draft-preview evaluator call it"; "holds all three faces" → "holds all FOUR faces"; plus one new bullet naming preview-evaluator.ts and its consequence. That is F1's requested fix, not a relabel.
  • The corrected enumerations are true on the tree: resolveAnalyticsDateRangeString has four non-test call sites (packages/drivers/driver-memory/src/memory-analytics.ts:1534, packages/services/service-analytics/src/strategies/native-sql-strategy.ts:511, .../objectql-strategy.ts:1712, .../preview-evaluator.ts:332); the kit registers four faces (one in the driver-memory runner, three in FACES of packages/services/service-analytics/src/__tests__/analytics-date-range-conformance.test.ts). The new bullet's "a valid last_30_days selected NOTHING there, silently" is measured: the merge-base evaluator returns [] for last_30_days and for today over rows inside the window.

② Clause-② surface — unchanged; lowerPreviewDateRange is not published

  • Core index diff vs merge base: exactly two export * from lines (./utils/analytics-date-range.js, ./utils/analytics-date-range-conformance.js). Spec diff: one PROVENANCE_WAIVERS row plus a comment edit. packages/core blobs are identical between cd14a59b4e and head (analytics-date-range.ts ceec30e7a1, conformance e0b8502a05, index.ts 9860d2ffe0), so the surface the baselines counted is byte-identical.
  • packages/services/service-analytics/package.json and src/index.ts are not in the PR diff (empty stat vs merge base). Manifest measured: exports maps "." only → ./dist/index.{js,cjs,d.ts,d.cts}; files = dist, README.md, CHANGELOG.md (no src); build is the root tsup.config.ts with entry: ['src/index.ts'], splitting: false. src/index.ts contains no re-export from preview-evaluator; the only non-test importer of that module is analytics-service.ts:62, importing evaluateAnalyticsQueryOverRows alone. lowerPreviewDateRange / PreviewDateRangeWindow appear only in preview-evaluator.ts and its own test file. Not reachable from any published entry; levels need no re-judging.

③ D1 wiring — behaviourally correct; refusal is exactly the ruled one

  • Explicit [a, b] byte-identical, merge-base evaluator vs head evaluator over probe rows: bare-day window ['2026-09-01','2026-09-30'][endday_late, endday_start, lo] on both (dashboard 的日期区间上界打在 datetime 列上丢失当天数据 —— 默认配置即命中 #3777 widening kept: whole 09-30 in, 10-01 out); instant window → [lo, up] on both (driver-memory analytics dateRange upper bound is INCLUSIVE for a full-timestamp end, so the first instant of tomorrow counts as today #16179 inclusive upper: up at the bound kept, +1ms excluded); macro-token array and one-element array identical too (see ⑤).
  • today byte-identical before/after: merge-base driver-memory parseDateRangeString('today', tz) vs head resolveAnalyticsDateRangeString('today', {timezone}) — 4 instants × 7 zones, 28/28 SAME, including 2026-09-09T22:00Z where Asia/Shanghai is already 09-10 (2026-09-09T16:00Z → 2026-09-10T16:00Z), 2026-09-09T03:00Z where America/New_York is still 09-08 (2026-09-08T04:00Z → 2026-09-09T04:00Z), the New York spring-forward day (2026-03-08T05:00Z → 2026-03-09T04:00Z, 23 h), Pacific/Kiritimati (+14), and an unknown zone degrading to UTC. Discriminating control passed. Head driver-memory's parseDateRangeString is a one-line delegation to that resolver (read at memory-analytics.ts:1533-1536), and the preview face's string arm is the same delegation (preview-evaluator.ts:331-334).
  • Refusal width: the face accepts iff isDateRangePresetName (packages/spec/src/data/date-range-presets.ts:63-64, DATE_RANGE_PRESETS.includes), the same 13-name set as the schema's string arm z.enum(DATE_RANGE_PRESETS) (analytics.zod.ts:272, union at :326-327). Measured: all 13 presets accepted; 'not a range at all', 'Last 7 Days', 'last 7 days', 'TODAY', 'today ', '2026-01-20', '2026-01-20T00:00:00.000Z', 'last_3_days' all throw code: 'ANALYTICS_DATE_RANGE_UNRECOGNIZED', status: 400 with the spec wording. Empty string and undefined still skip the filter on both refs.
  • What used to work and now 400s at this face: exactly the non-preset strings — measurably only '2026-01-20' (bare-day dialect) selected rows at merge base (["day"]); every other refused spelling already selected nothing. That is the refusal D1 ordered ("call the same lowering + refusal there") and the changeset discloses it (BREAKING paragraph; FROM→TO row dateRange: '2026-01-20'['2026-01-20','2026-01-20']). Not wider than ruled.
  • Live path propagates: queryDatasetDatasetExecutor.executeresolveSelectionTokens (a non-{…} string passes through unchanged, filter-tokens.ts:376-377) → buildQuery (forwards dateRange, only fills granularity) → service.query proxy → evaluateAnalyticsQueryOverRows. git grep '\bcatch\b' in dataset-executor.ts = 0 hits (control: the same file greps service.query( twice); the only catch on the preview branch of analytics-service.ts wraps draftRowsResolver, not the evaluator. Both files unchanged by the PR.

④ Nothing weakened to reach green

  • git grep -o 'it\.todo' in packages/drivers/driver-memory/src/ at head = 0; control at merge base = 4 (dst 2, timezone 1, utc-window 1). test.todo/describe.todo = 0.
  • Added lines of the full diff: 0 hits for \.(skip|only|todo|fails)\b|x(it|describe|test)\(|skipIf|runIf|SKIP; control: the same regex over all diff lines hits the four removed it.todo lines, and it.skip exists elsewhere in the repo at head (3 files). No vitest config is among the 15 changed files.
  • Non-vacuity of the three end-to-end cases (analytics-date-range-conformance.test.ts, second describe): the rolling and calendar cases compare previewSelects(...) against fixed literal id lists (['at_end','at_start'], ['at_start','just_inside_end']); the refusal case asserts thrown?.code and thrown?.status on a variable that stays undefined when nothing throws. My harness shows the merge-base evaluator — the exact shape the author's ablation restores — returns [] / does not throw for those inputs, so the assertions cannot pass without the wiring. The explicit-[a,b] case is a preservation pin and passes either way, as the author's table says. FACES has three entries (so it.each is not empty); the kit's live-harness control is real (a broken face yields 33 findings, the old [range,range] fallback yields 44, in my run).

⑤ One-element array — (a) pre-existing, untouched; follow-up card

  • Schema: z.union([AnalyticsDateRangePresetSchema, z.array(z.string())], …) at analytics.zod.ts:326-327; file blob identical at merge base and head, so ['2026-01-01'] is schema-valid on origin/main today.
  • ObjectQL: const [start, end = start] = range; (merge base objectql-strategy.ts:1693) → const [start, end = start] = td.dateRange; (head :1730) — the point degeneration is unchanged.
  • Preview: measured identical merge base vs head — ['2026-01-01'] selects [at_start, mid, far] (2099 admitted) on both refs; on both, the upper bound is the string 'undefined~' because nextUtcCalendarDay returns null for any non-YYYY-MM-DD input (calendar-day.ts:43-46), so nothing was introduced or worsened by D1.

CI on head

40 check runs on aaf953b49d, all completed: 35 success, 5 skipped (Console Pin Gate, Build Docs, Packed-tarball smoke opt-in, and the 11:10Z re-run's Auto Label / Check PR Size), 0 failed, 0 in progress. Lint & Repo Gates, Type Check · workspace, Check Changeset, Temporal Conformance (live PG + MySQL) all success.

Findings

  • N1 (non-blocking) — Four source comments still enumerate three faces after the fourth was wired: packages/core/src/index.ts:83 ("two SQL strategies"); the analyticsDateRangeUnrecognizedError docstring in packages/core/src/utils/analytics-date-range.ts ("called by driver-memory's cube face and by both service-analytics strategies"); the waiver reason at packages/spec/src/api/error-code-ledger.zod.ts:1596 ("BOTH service-analytics strategies" — a string value that ships in dist, but not behaviour); packages/drivers/driver-memory/src/memory-analytics.ts:1531 ("all three call one constructor"). The release input (changeset) is correct; these are code comments, none in the delta.
  • N2 (non-blocking, prose) — The new changeset bullet's "while the published chart beside it answered a real window" describes the mid-branch state after the other three faces were wired, not the pre-release state (the changeset's own table shows the published chart answered all-of-history or a nonsense point window before). Optional rewording; the user-visible claim (preset selected nothing, now a real window) is measured true.
  • N3 (observation) — "call it" is transitive on all four faces: outside tests only analytics-date-range.ts:234 calls analyticsDateRangeUnrecognizedError directly, inside resolveAnalyticsDateRangeString. Same wording the baselines passed; the "one refusal" property it asserts holds.
  • N4 (follow-up card, non-blocking) — The one-element-array divergence in ⑤: pre-existing on origin/main, byte-identical before/after this PR on the preview face, unchanged on ObjectQL.
  • Director's F2 and F4 stand as recorded (files unchanged); F3 (stale PR body) is now addressed by the body's PATCH ROUND section.

Verdict: PASS WITH FINDINGS — all findings non-blocking. Nothing between 070eb30e9e and aaf953b49d disturbs either baseline verdict: the delta is the F1 changeset edit alone, levels and package set byte-identical, no source moved, the clause-② basis unchanged, the D1 wiring behaviourally correct with the refusal exactly the schema's set, no test weakened, and the one-element divergence pre-existing. Measured on aaf953b49d4e43af0c9089db1aaf8c7ed3c68f82.


Seat disposition

ACCEPT — landing proceeds. No blocking finding, and the two baselines are undisturbed. Both clause-② carriers are cleared in the same stroke as this comment, which is their provenance.

PM dispatch seat · domain:engine · session session_01XTBcV7zZHmokdyQgXjbyEU · R1 · reading taken 2026-09-09T11:3xZ


Generated by Claude Code

@os-sam
os-sam marked this pull request as ready for review September 9, 2026 11:31
@os-sam
os-sam enabled auto-merge September 9, 2026 11:31
@os-sam
os-sam added this pull request to the merge queue Sep 9, 2026
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/xl tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Analytics drivers (memory, SQL) align dateRange string handling to the closed preset vocabulary (driver half of #16041)

4 participants