Skip to content

feat(spec)!: close timeDimensions[].dateRange's string arm to the date-range preset vocabulary (#16041) - #16618

Merged
huangyiirene merged 8 commits into
mainfrom
claude/issue-16041-analytics-daterange-closed-vocabulary
Sep 7, 2026
Merged

feat(spec)!: close timeDimensions[].dateRange's string arm to the date-range preset vocabulary (#16041)#16618
huangyiirene merged 8 commits into
mainfrom
claude/issue-16041-analytics-daterange-closed-vocabulary

Conversation

@huangyiirene

@huangyiirene huangyiirene commented Sep 7, 2026

Copy link
Copy Markdown
Collaborator

Fixes #16041

Clause-②: yes

What this PR does — the spec half of the #16041 ruling (decision batch #57, option A)

Maintainer ruling, verbatim: 「本项目以协议为基准。所以开发应该对其协议,协议有问题应该立卡修改协议」 (reply 「同意」).

AnalyticsQuerySchema.timeDimensions[].dateRange had a bare z.string() arm whose only documented example, "Last 7 days", was a value no driver could parse: it fell through driver-memory's [range, range] fallback and matched every Date-typed row (a Date compares above a String under BSON cross-type ordering), while the SQL strategies read the same string as a single ISO day. Same input, opposite wrong answers, HTTP 200 on both.

  1. Vocabulary closed, derived, never copied. AnalyticsDateRangePresetSchema = z.enum(DATE_RANGE_PRESETS) in packages/spec/src/data/analytics.zod.ts, built off data/date-range-presets.ts — the module whose header records the three drifting copies spec: dashboard date filter 的 defaultValue 没有作者时校验 —— 拼错的预设名要到浏览器控制台才被发现 #4614 retired. today is that tuple's first member, so the ruling's "presets plus today" is this enum with no second source. The pin asserts AnalyticsDateRangePresetSchema.options equals [...DATE_RANGE_PRESETS]; the refusal text and the field's .describe() are both built from DATE_RANGE_PRESETS.join, so neither the message nor the generated reference page can become a fourth copy.
  2. Refuse, do not widen. AnalyticsDateRangeSchema = z.union([presets, z.array(z.string())], { error }) raises ONE invalid_union issue at timeDimensions.N.dateRange carrying analyticsDateRangeRefusalMessage(input) — the value, the vocabulary, the array spelling for an explicit window. Spec exports isAnalyticsDateRangeRefusalIssue, structural (the union's own issue code at the path the schema declares), never message sniffing. The runtime door (packages/runtime/src/domains/analytics.ts, serving /analytics/query and /analytics/sql) lifts it into the ADR-0112 envelope 400 ANALYTICS_DATE_RANGE_UNRECOGNIZED — a new ERROR_CODE_LEDGER member registered under @objectstack/runtime, the door being the emitter (the FLOW_* "the door names the wire vocabulary" precedent). Lifted only when every issue is that refusal; a body wrong elsewhere too stays the generic 400 VALIDATION_FAILED + details.fields[]. The analytics service is never reached.
  3. Example corrected. The schema comment and the compiled @example (marked os:check, so check:skill-examples type-checks it against the built package) now show dateRange: 'last_7_days'. 'Last 7 days' is not in the vocabulary — checked, not assumed: isDateRangePresetName('Last 7 Days') is pinned false in date-range-presets.test.ts, and the list is snake_case and case-sensitive. The three in-repo authors of 'Last 7 days' — all spec tests — are re-spelled. The wire code is written into the field's .describe() (so into the generated references/data/analytics.mdx and references/api/analytics.mdx tables) and into the JSDoc.

Type narrowing: AnalyticsQuery['timeDimensions'][n]['dateRange'] is now the preset union | string[] | undefined, pinned with a @ts-expect-error in a file the spec test-layer tsc program compiles.

Base and ancestry

Branched from origin/main at ffca0df9cb (#16575). git merge-base --is-ancestor 90e7e6de114af2586da89dd417101b24db716d39 HEAD exits 0, so the api-surface generator's one-row-per-kind format is what this branch regenerated (api-surface/data.json: +6 rows, 0 breaking).

Changeset grade — measured on this diff, not inferred

  • major: a committed probe changeset declaring "@objectstack/spec": major made check-changeset-no-major.mjs --base origin/main exit 1 with ⛔ This PR introduces changeset(s) that declare a major bump (.changeset/pre.json absent, enforce path live). The probe commit was reset away and never pushed. An uncommitted probe read clean — the gate diffs committed HEAD, which is why the probe had to be committed to count.
  • Shipped: minor (spec + runtime) with the **BREAKING** banner and the explicit adr-0087: registered analytics-time-dimension-date-range-vocabulary-closed disposition (an HTML-comment line inside the changeset file — the form that gate reads). check-adr-0087-registration.mjs --base origin/main exit 0: 1 declared-breaking changeset(s), each carrying an ADR-0087 disposition … [BREAKING+bang] registered analytics-time-dimension-date-range-vocabulary-closed (new here: …) — a non-vacuous read: the gate saw the declaration and verified the id is new in this diff. check-changeset-no-major✓ This diff introduces no major bump (its LEVEL AXIS reads the Clause-②: line above from the PR payload in CI; locally it printed NOT MEASURED, no payload). check-empty-changeset✓ … 1 declaring changeset(s) added.
  • ADR-0087: semantic entry 18.analytics-time-dimension-date-range-vocabulary-closed (FROM → TO for the display spelling, driver-memory's last N UNIT dialect and SQL's bare-ISO single day, each to a preset name or a [start, end] array). registry.ts regenerated (check:migration-registry ✓). spec-changes.json and protocol-upgrade-guide.md do not project step 18 yet — the Refuse the declared relative-date preset vocabulary as a bare temporal comparand at publish time — the ruled C half of #8690, carved out for the spec seat #8793 precedent entry is absent from both as well — and both gates are ✓.

Evidence

Pins with negative controls (source-level ablation, restored with blob-hash proof and git diff HEAD empty):

  • packages/spec/src/data/analytics-date-range-closed-vocabulary.test.ts (11 cases): refusal of "Last 7 days" with exactly one issue at ['timeDimensions', 0, 'dateRange'], every retired spelling (driver-memory's, SQL's single-day, case and near-miss variants, the custom sentinel, a macro token), both request schemas, non-arm values described by type, the predicate's structure, ledger membership, type narrowing. Leg A restored the bare z.string() arm (anchor 1→0, blob b1e84ba4… ≠ HEAD e573905a…): 4 refusal cases red, 7 acceptance cases green — the pin would have been red on the unfixed schema.
  • packages/runtime/src/analytics-daterange-refusal-envelope.test.ts (6 cases): 400 ANALYTICS_DATE_RANGE_UNRECOGNIZED on /analytics/query and /analytics/sql, service not called, declaredCode absent, the body parses against ApiErrorSchema, a preset passes the door with the original body forwarded untouched, a multi-field failure stays VALIDATION_FAILED. Leg B restored BASE's door (literal count 2→0, blob equals the BASE blob): 3 code assertions red ('VALIDATION_FAILED' observed), 3 pass-through cases green.

Runs at bf059cf478 (under os-verify-lock, VERDICT command-exit 0 unless stated):

  • spec: 8 test files / 235 tests pass (the new pin, the three re-spelled fixtures, the ledger test, the ADR-0122 pin file, date-range-presets, src/migrations); pnpm --filter @objectstack/spec typecheck exit 0 (tsc + scripts + test layer; the 54-file / 261-error held debt is unchanged).
  • runtime: 3 test files / 29 tests pass (new door pin, dispatcher-validation-error, dispatcher-plugin.error-envelope); pnpm --filter @objectstack/runtime typecheck exit 0.
  • pnpm --filter @objectstack/service-analytics typecheck exit 2 at bf059cf478 — one error, src/__tests__/objectql-daterange.test.ts(187,53): error TS2322: Type '"2026-01-20"' is not assignable to type 'string[] | "today" | … | undefined' — the fixture authored the bare-date spelling the narrowing retires. Repaired at 396e627911 under a one-line fence lift (PM authorisation, option A of the report): the fixture value is now dateRange: ['2026-01-20', '2026-01-20'], plus a comment above the test recording that its title names the bare-string dialect this PR retires at the schema and that retiring the test belongs to Analytics drivers (memory, SQL) align dateRange string handling to the closed preset vocabulary (driver half of #16041) #16322. Nothing else in packages/services, driver-memory or driver-sql moved — git diff --name-only bf059cf478 396e627911 lists exactly that one file. Reproduce-then-green: the same command exit 2 on the unrepaired tree (the line above), exit 0 after (VERDICT command-exit 0). The test file: 15/15 pass. ObjectQLStrategy.dateRangeBounds lowers both spellings through const [start, end = start] = range, so the expected $gte/$lte bounds are byte-identical — no changed line mentions gte, lte, toEqual or rows. The package's full pnpm test: 96 files / 2163 tests pass; the spec pins (235/235) and runtime pins (29/29) re-ran unchanged.
  • Gates, verdict lines quoted in the report: check:generated ✓ 16/16 (api-surface, authorable-surface, docs, export-origins, declaration-map, migration-registry, spec-changes, upgrade-guide, strictness-ledger, liveness, skill-docs/refs, react-blocks, meta-url-spelling, test-typecheck); error-code-provenance ✓; error-code-casing ✓; dispatcher-error-vocabulary ✓; nul-bytes ✓ (8170 files); spec-parsed-alias ✓ (815 pinned isomorphic — two new pins Iso869 / Iso870); skill-examples ✓ (258 marked examples; 10 spec-source TSDoc blocks compiled, this one included); quick-reference-counts ✓; doc-authoring ✓; docs-single-h1 ✓; cross-package-test-inputs ✓; test-source-alias ✓; type-check-coverage ✓; route-envelope ✓; closing-keyword-parity ✓; changeset-gate-self-tests ✓; ci-filter-parity ✓; corpus-claim-drift ✓; keyed-text-bounds ✓; published-files ✓; merge-driver ✓; docs-redirects ✓ — 59 of the 103 families dispatch-gates.mjs derives for this diff. check:type-check-debt exit 3 = PREREQUISITE NOT MET (full closure not built) → NOT MEASURED, not a finding. The 44 unrun derived families (driver/plugin census and shape checks, sourcemaps, vendor stamps, CJS loads, release rehearsal, self-tests of scripts this diff does not edit) touch nothing here and are CI's half.
  • Regenerated diffs read, not trusted on exit code: json-schema.manifest/data.json (+2 ids), api-surface/data.json (+6 rows, 0 breaking), export-origins / declaration-map (+6 / +4), registry.ts (+58 lines, the one entry), the reference pages (the enum in both timeDimensions rows, two nested-shape tables, AnalyticsDateRange / AnalyticsDateRangePreset sections, ledger doc +1, index counts 1574 → 1576). No JSDoc detached; no page rewritten beyond those.

Consumer sweep

  • In-repo authored dateRange strings on timeDimensions: 3 × 'Last 7 days' at ffca0df9cb, all packages/spec tests — triage counted 2 on 932acc3d; the third is packages/spec/src/data/analytics.test.ts:354, same class, same conclusion — re-spelled here; plus the service-analytics '2026-01-20' fixture, re-spelled in the array form at 396e627911 (see Evidence). No published dashboard, example app, skill or doc page authors the string arm.
  • objectui 51a402f: every timeDimensions[].dateRange author uses the array form (DatasetWidget.tsx types it [string, string]; queryDataset.test.ts:473; DatasetWidget.compareTo.test.tsx:122).
  • REST /analytics/dataset/query's selection.timeDimensions is typed from AnalyticsQuery but not Zod-parsed at that door, so a string there still reaches dataset-executor (fenced) — noted for Analytics drivers (memory, SQL) align dateRange string handling to the closed preset vocabulary (driver half of #16041) #16322.

Not in this PR

…-range preset vocabulary (wip)

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01T6HeZvT9wdSJD1ZxJb5Eno
…ons for the closed dateRange vocabulary (wip 2)

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01T6HeZvT9wdSJD1ZxJb5Eno
…and reference docs for AnalyticsDateRangePreset (wip 3)

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01T6HeZvT9wdSJD1ZxJb5Eno
…ompile the schema example (wip 4)

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01T6HeZvT9wdSJD1ZxJb5Eno
@github-actions github-actions Bot added the size/l label Sep 7, 2026
@github-actions

github-actions Bot commented Sep 7, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/runtime, @objectstack/spec, touching 15 documentable anchor(s). ⚠️ 4 changed file(s) yielded no anchor (packages/spec/api-surface/data.json, packages/spec/declaration-map/data.json, packages/spec/export-origins/data.json, …), 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 dateRange (literal, a string literal in isAnalyticsDateRangeRefusalIssue))
  • content/docs/kernel/contracts/data-engine.mdx (via ERROR_CODE_LEDGER (symbol, a top-level const object))
  • content/docs/ui/dashboards.mdx (via dateRange (literal, a string literal in isAnalyticsDateRangeRefusalIssue), last_7_days (literal, a string literal in AnalyticsQuerySchema; a string literal on a changed line))

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

  • content/docs/releases/v15.mdx (via dateRange (literal, a string literal in isAnalyticsDateRangeRefusalIssue))
  • content/docs/releases/v16.mdx (via dateRange (literal, a string literal in isAnalyticsDateRangeRefusalIssue))
  • content/docs/releases/v17.mdx (via ERROR_CODE_LEDGER (symbol, a top-level const object), invalid_union (literal, a string literal in AnalyticsDateRangeSchema; a string literal in isAnalyticsDateRangeRefusalIssue), timeDimensions (literal, a string literal in isAnalyticsDateRangeRefusalIssue; 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
  • 4 changed file(s) yielded no anchor (packages/spec/api-surface/data.json, packages/spec/declaration-map/data.json, packages/spec/export-origins/data.json, …) — pages documenting those are invisible to this run
  • 1 anchor(s) matched too much of the corpus to be a work list: created_at (literal, 33 pages)
  • 4 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 61 of 219 client-bound route-ledger rows — the other 158 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 158: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 56 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 102 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 — 135 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 4d728680c0cd5fd1660aa1a57271bc7f084df559packageMentionDocs.

Which tree this was computed on

This run read content/docs from 84a972c9831cfe979252bbcfc3f5638d657a8df3 — the merge of head 080052ce832fbe175b6fdf74bcd7fde22d8ac6cc into base 4d728680c0cd5fd1660aa1a57271bc7f084df559, 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 84a972c9831cfe979252bbcfc3f5638d657a8df3 && git checkout 84a972c9831cfe979252bbcfc3f5638d657a8df3
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 4d728680c0cd5fd1660aa1a57271bc7f084df559 080052ce832fbe175b6fdf74bcd7fde22d8ac6cc && git checkout -B drift-repro 4d728680c0cd5fd1660aa1a57271bc7f084df559 && git merge --no-ff 080052ce832fbe175b6fdf74bcd7fde22d8ac6cc

node scripts/docs-audit/affected-docs.mjs --json 4d728680c0cd5fd1660aa1a57271bc7f084df559

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

… the array form

The closed string vocabulary for timeDimensions[].dateRange retires the
bare-date spelling this fixture authored, so the package's required
TypeScript Type Check went red on one line (TS2322 at line 187). The
fixture now writes the same single-day window as ['2026-01-20',
'2026-01-20']; the strategy lowers both spellings to the identical
$gte/$lte bounds, so the expected values are untouched. The test's
title still names the retired dialect on purpose: a comment above it
records that retiring the test belongs to the driver-alignment card,
together with the strategy's degeneration.

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

Copy link
Copy Markdown
Collaborator Author

Contract review at CONTRACT_REVIEW_TIER (claude-fable-5-1) @ 396e627911contract PASSES on every leg; one landing blocker

All five contract questions answered by measurement, not by reading. The block is a required-CI red that the author's sweep could not have seen, in a package the fence forbade them from entering.

⛔ B1 — @objectstack/driver-memory typecheck is red at this head (measured, exit 2)

Three TS2322, all in tests this PR never touched (0 lines in ffca0df9cb..HEAD), each naming the union this PR introduces:

src/memory-analytics-date-range-dst.test.ts:147       dateRange: range              (range: string)
src/memory-analytics-date-range-timezone.test.ts:118  dateRange: opts.range ?? 'today'
src/memory-analytics-date-range-utc-window.test.ts:120 dateRange: range

It reaches CI: driver-memory's tsconfig.json includes src/**/*, and lane 3 of the required TypeScript Type Check runs turbo run typecheck --filter='./packages/*/*', which covers packages/drivers/driver-memory. ⇒ queue ejection, not cosmetic.

Why the author's sweep missed it, and why that is not a lapse: those tests author the value through an identifier (dateRange: range), so a literal grep for the retired spellings cannot see them — and only spec, runtime and service-analytics were typechecked. The reviewer found it by typechecking every package that references timeDimensions; runtime, rest and driver-sql all come back exit 0, so this is the only one.

⚠️ I am not resolving this the way I resolved the service-analytics fixture, and the difference matters. There it was one line, one literal, and the array form produced byte-identical bounds — a spelling. Here the values are the retired relative dialect ('last 7 days', 'last 3 days', 'last 2 weeks', 'last 1 year', 'last 1 month', 'last 3 months', 'last 1 week'none a preset), and the type error is true: once this lands, those are values the schema refuses, so a driver test feeding one directly exercises a path production can no longer reach through the door. That is real signal, and retiring it is #16322's business.

⇒ A repair is dispatched with the fence lifted only for a minimal, honest fix, and with a bare silencing cast explicitly refused: if the only way through hides the signal with nothing to explain it, the dev stops and this PR waits. Leaving it unarmed overnight is an acceptable outcome; a silenced true type error is not.

The five questions — all PASS, all measured

1. The enum is genuinely derived. Planting a probe preset in date-range-presets.ts (blob 8c18caad0ba90054229) moved everything together: enum options 13→14, the refusal message, the union's invalid_union message, the field .describe() (493→519 chars), the z.toJSONSchema projection, and the schema then accepted the name. So there is no fourth copy, and the module's three-drifting-copies history does not repeat. today is the tuple's first member, not bolted on.

2. The door discriminates correctly. An 8-case probe: bad dateRange plus limit:"ten" / an unknown top-level key / a bad granularity / wrong-typed measures400 VALIDATION_FAILED carrying both fields; two bad, one-good-one-bad, 42, ['2026-01-01', 3]400 ANALYTICS_DATE_RANGE_UNRECOGNIZED. The service was called 0 times in all eight. The predicate is structural — reads code and path shape only, and the door has zero references to message text, so a reword cannot silently break it. Leg B (BASE door on disk, blob proven ≠ HEAD) reds exactly 3 with VALIDATION_FAILED observed.

3. ⭐ The ADR-0087 disposition is non-vacuous — and this is the contrast worth recording. breakingDeclaration{breaking: true, signals: ["BREAKING", "bang"]}; findMigrationPrescription → branch from-to-label on the ## FROM → TO block; readDispositionregistered analytics-time-dimension-date-range-vocabulary-closed; gate exit 0, "new here". Every signal fired. A sibling PR merged today carries an identical-looking green that is a genuine detector miss — this one is not, and the difference is the explicit FROM → TO framing.

major confirmed refused on this diff by a committed probe (exit 1, ⛔ … declare a major bump), with the uncommitted probe reproducing the void-leg mechanism — the gate diffs committed HEAD, so an uncommitted probe reads falsely clean. That trap was self-caught by the author and independently reproduced here.

4. The corrected example holds. last_7_days is tuple member 11; matching is exact and case-sensitive; isDateRangePresetName('Last 7 Days') is pinned false. All three in-repo authored occurrences re-spelled (the base census found 3, one more than triage's older count, all tests); remaining Last 7 days hits at HEAD are refusal fixtures, prose, and two page:header false positives. Repo-wide there are zero non-preset string authors on timeDimensions; objectui at its pin uses the array form only.

5. Scope holds. git diff over packages/drivers and packages/services is exactly the one authorised fixture; parseDateRangeString has zero diff. The bounds claim is confirmed structurally — dateRangeBounds lowers both spellings through const [start, end = start] = range — and the pre-repair fixture reproduces TS2322 (187,53) exit 2 on disk.

Advisories (none blocking)

  1. The door's lift is issues.every(...), vacuously true on an empty list. Zod never produces one, so this is theoretical; issues.length > 0 && would make it explicit.
  2. The field's .describe() says "any other string is refused", while measured behaviour also lifts non-string and non-array values. Cosmetic wording.
  3. The ADR-0087 entry spells the thirteen names in prose — a ledger snapshot, not a code copy. Acceptable, but a fourteenth preset will read stale there by design.
  4. /analytics/dataset/query is still not Zod-parsed at its door — the author's own finding, already routed to Analytics drivers (memory, SQL) align dateRange string handling to the closed preset vocabulary (driver half of #16041) #16322.

Not measured: @objectstack/dogfood typecheck (PREREQUISITE NOT MET, 169 module-resolution errors on unbuilt connectors; its only dateRange hit is a comment, so it would have decided nothing) · driver-memory typecheck at base (not rebuilt — causation rests on the error text naming the new union) · check:type-check-debt.

Held unarmed pending B1 and the forward merge.


Generated by Claude Code

…ed tree

Regeneration commit for the forward merge of origin/main (554a160) into
this branch, kept apart from the merge commit so the two can be read
separately. `gen:schema` + `gen:docs` on the merged tree changed exactly one
file: the references index, whose counts are now the union of both sides —
main's ResumeFailureDetails (API 436 -> 437) plus this branch's
AnalyticsDateRange / AnalyticsDateRangePreset (Data 166 -> 168), total
1575 -> 1577. No other generated artifact moved.

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

Copy link
Copy Markdown
Collaborator Author

Forward merge landed (2026-09-07 landing round) — and one required red this branch still owes

Two commits, kept apart on purpose (scripts/pm/os-regen-merge.sh sequence):

  • 1add8f329fMerge remote-tracking branch 'origin/main' at 554a16037e. The only paths both sides touched since the merge base ffca0df9cb are two generated files: content/docs/references/index.mdx (the os-regen driver deferred it; the driver-free bare-clone probe, which is what GitHub runs, read CONFLICT (content) there before the merge) and packages/spec/src/migrations/registry.ts (auto-merged). No non-generated file overlapped, so no side was picked by judgement.
  • d8a3fd2d08gen:schema + gen:docs on the merged tree. Exactly one file moved: the references index, and its diff is the union of both sides — main's ResumeFailureDetails (API 436 → 437) plus this branch's AnalyticsDateRange / AnalyticsDateRangePreset (Data 166 → 168), total 1575 → 1577. No JSDoc detached, no other page rewritten, no authorable-surface anchor moved. The pre-commit hook printed content/docs/references/index.mdx — current / all deferred artifacts are current — marker cleared.

Gates on d8a3fd2d08: check:generated✓ All 15 generated artifacts are up to date (migration-registry, spec-changes, upgrade-guide, authorable-surface, api-surface, export-origins, declaration-map, docs, strictness-ledger, liveness, test-typecheck and the rest); check-adr-0087-registration --base origin/main✓ 1 declared-breaking changeset(s), each carrying an ADR-0087 disposition; check-changeset-no-major --base origin/main✓ This diff introduces no major bump (LEVEL AXIS NOT MEASURED locally, no PR payload); check-empty-changeset --base origin/main✓ 1 declaring changeset(s) added; check:nul-bytes OK (8191 files); check:merge-driver ✓; docs-audit affected-docs / drift-comment / docs-section-name ✓. Re-measured on the merged tree under the verify lock: spec typecheck exit 0, spec pins 8 files / 235 tests pass, runtime pins 3 files / 29 tests pass (after rebuilding the runtime closure — a first run failed to load on a stale @objectstack/observability dist, NOT MEASURED, then rebuilt and green). mergeable_state moved from dirty to blocked (waiting on checks).

⚠️ Not repaired, deliberately — @objectstack/driver-memory is red on this branch, and it is a runtime red, not only a type error. pnpm --filter @objectstack/driver-memory typecheck exits 2 with three TS2322 in memory-analytics-date-range-dst.test.ts:147, -timezone.test.ts:118, -utc-window.test.ts:120 — but every one of those helpers routes its fixture through asQuery = (input: AnalyticsQuery) => AnalyticsQuerySchema.parse(input) before calling MemoryAnalyticsService.query, so the values they feed ('last 7 days', 'last 3 days', 'last 2 weeks', 'last 1 month', 'last 3 months', 'last 1 year', 'last 1 week', and the utc-window fallback probe) are refused at the schema door: running the three files gives Test Files 3 failed (3) · Tests 15 failed | 37 passed (52), each failure a ZodError: invalid_union listing the 13 presets. The 'today' cases pass. asQuery was introduced by the #15825 (#16047) and #16042 (#16174) landings, so it is not an incidental annotation, and MemoryAnalyticsService.query itself takes AnalyticsQuery. A cast alone would green the typecheck and leave the 15 runtime failures; making them pass would require routing those fixtures around AnalyticsQuerySchema.parse — a change to what the fixture exercises, inside #16322's territory. Per the landing brief's STOP fence, nothing in driver-memory was edited; the decision is recorded in the os-dev-report on #16041. CI on this head (the first CI this PR gets — a dirty PR had zero check runs) is expected to show TypeScript Type Check and Test Core red on driver-memory for exactly this reason.

PR left draft; nothing armed.


Generated by Claude Code

Copy link
Copy Markdown
Collaborator Author

⛔ HELD — not arming. The forward merge is done; B1 is not fixable inside this card's fence.

The contract is verified and correct (review 5571543935, all five questions PASS by measurement). This is a landing block, and it is deeper than it looked.

What landed

Forward merge complete. Merge commit 1add8f329f (origin/main 554a16037e), then gen:schema + gen:docs as a separate commit d8a3fd2d08 so the regeneration is reviewable apart from the merge. The only both-sides paths since the merge base were the two generated files: references/index.mdx (deferred by the os-regen driver; CONFLICT (content) under the driver-free bare-clone probe, i.e. GitHub's actual condition) and migrations/registry.ts (auto-merged). No non-generated overlap ⇒ no side was chosen by judgement. The regenerated diff was read line by line: the union of main's ResumeFailureDetails (API 436→437) and this branch's AnalyticsDateRange / AnalyticsDateRangePreset (Data 166→168), total 1575→1577. No JSDoc detached, no other page rewritten.

⭐ Why B1 stops here — my own premise was falsified, and that is the finding

I authorised a narrow fence lift on the guess that "these tests call the driver directly, not through the schema door, so the narrowed type may be incidental." That is false, and it was measured.

Each of the three helpers builds an AnalyticsQuery literal and passes it through

asQuery = (input: AnalyticsQuery): AnalyticsQuery => AnalyticsQuerySchema.parse(input)

before service.query(...). asQuery was introduced deliberately by the #15825 and #16042 landings — a fixture step, not an incidental annotation.

The three files are runtime-red, not merely tsc-red: 15 failed / 37 passed (52), every failure a ZodError: invalid_union from the schema door listing the 13 presets. (The 37 green cases feed 'today', the one token both vocabularies share.)

So a commented cast would green tsc and leave 15 runtime failures — it fails the "tests must still pass unchanged" condition I set. Making them pass means routing the dialect fixtures around AnalyticsQuerySchema.parse, which changes what the fixture exercises. That is #16322's territory and I did not authorise it. The dev stopped rather than doing it, which is exactly right.

And the cost of doing it anyway is not neutral: the utc-window case pins "the unrecognised-range fallback carries no calendar" — i.e. it asserts the silent-widening fallback that card #16041 exists to abolish. Routing it around the door would preserve a pin on the defect. ⛔ I am not authorising that at 14:00 with the maintainer away.

⭐ The real finding, and it is about the ruling, not this PR

The ruling's spec-first ordering is not executable as written. Batch #57 put the spec half first and made #16322 Blocked-by it — but the spec half cannot pass its required checks while the driver's own pins feed the retired dialect through the schema door. The two halves are coupled by driver-memory's fixtures, in the direction opposite to the declared dependency.

That is a sequencing decision for the maintainer, not something to resolve by widening a fence. The options, with their costs, are on card #16041 (report comment 5571833149) and summarised in my note there.

CI on the merged head — first real CI this PR has ever had

⚠️ At 396e627911 this PR had zero check runs: a dirty PR gets no merge ref, so every "CI red" before now was a local reading. The first real run is on d8a3fd2d08, and it confirms the prediction:

check result
TypeScript Type Check failure — predicted (driver-memory)
Type Check · workspace failure — same cause
Test Core (2/6) failure — predicted (the 15 driver-memory cases)
Temporal Conformance (live PG + MySQL) failure⚠️ not predicted, and NOT diagnosed

On the last one, so it is not misreported: the only substantive line in the job log is a MySQL InnoDB error — Cannot create the table os_lv_sql_driver_11565_row_byte_budget_….os11565_narrow because the record size will exceed the maximum allowed size for a record — a driver-sql row-byte-budget fixture (#11565), naming a package this diff does not touch. That is the shape of a not-this-PR failure, but I have not established it: no base comparison, no re-run. ⛔ Recorded as undiagnosed, not as a flake — "flake" is not a root cause. It does not change tonight's outcome, since B1 blocks this PR regardless.

Still running at this writing: Lint & Repo Gates, Test Core (5/6), Test Core (6/6).

State

PR stays draft. Card #16041 keeps pm:dispatched and its assignee — the work is not done. ⚠️ references/index.mdx re-dirties often, so whoever resumes this will likely need one more forward merge at arming time.


Generated by Claude Code

Copy link
Copy Markdown
Collaborator Author

⚠️ Correction — Temporal Conformance IS this PR's red. I called it "not ours" twice, on a misread log.

I wrote that it failed on a driver-sql row_byte_budget fixture (#11565) and had "the shape of a not-this-PR failure." That is wrong, and the way it was wrong is worth recording because it cost two rounds.

What actually happened, established from get_workflow_job step conclusions rather than from the log:

  • step 12, "Run driver-sql suite against both live servers"success
  • step 14, "Run the non-SQL temporal backends under the skewed process zone"FAILURE

That step's log: process zone: America/New_York (offset 240), then FAIL src/memory-analytics-date-range-dst.test.ts …, Test Files 3 failed | 41 passed (44), Tests 15 failed | 1095 passed (1110). ⇒ The same 15 driver-memory failures, run under a skewed TZ.

The InnoDB line I quoted sits at line 4977 of a 9452-line log, inside the post-job MySQL service-container dump — 350-odd lines after the ##[error] at 4619. It is the server's echo of a passing negative test (sql-driver-11565-row-byte-budget.test.ts, "reports InnoDB page-limit refusals with the page limit, not the row budget") which expects that refusal. The identical line appears at line 4978 of main's green Temporal run (8341ed21ce, job 101772909902, success).

⇒ Reading a job log from the end puts a passing test's server-side error where the failure cause should be. The verdict lines are hundreds of lines earlier. Read get_workflow_job step conclusions before reading any log — that single habit would have prevented both misreads.

So all four reds are ONE defect

TypeScript Type Check · Type Check · workspace (failed only on @objectstack/driver-memory#typecheck exited (2)) · Test Core (2/6) (failed only on @objectstack/driver-memory#test) · Temporal Conformance (step 14, same 15 cases under TZ=America/New_York). Nothing else is wrong with this PR, and the prepared retirement greens all four — the three files re-run under that same zone give 37 passed | 15 todo.

⭐ A finding that outlives this PR: re-spelling was refused by measurement

Driving the built driver-memory dist directly with each of the 13 preset names over 5 probe rows (2020, 2026-08-31, 2026-09-05, now, 2099):

input result
today a real window — matched 1/5
all twelve other presets parseDateRangeString returns [name, name] — the fallback — matched 5/5, including 2020 and 2099
last 7 days / last 3 days (retired dialect) real windows — 3/5, 2/5

⇒ 0 of the 15 cases can be re-spelled, so retirement is option B's only executable form.

⚠️ And it says something about what landing spec-first leaves behind. After this PR, a valid preset such as last_30_days is accepted by the schema and then silently widens in driver-memory — matching every row, 2099 included. That is the same defect class card #16041 exists to abolish, relocated onto the newly-blessed vocabulary until #16322 aligns the parser.

It does not block this landing — triage established the parser is driver-memory-local, absent from driver-sql and service-analytics, so it is not on the production SQL path, which is why this card is p2 and not p1. But it makes #16322 materially more urgent than "cleanup", and I am recording it there.

Coverage this PR gives up, stated loudly

Retiring the 15 cases takes the last N UNIT arithmetic leg's DST coverage in driver-memory to zero until #16322 — all 13 driver-facing DST cells (day/week/month/year legs across America/New_York, Los_Angeles, St_Johns, Europe/London, Berlin, Australia/Sydney, Pacific/Auckland, Chatham, America/Santiago), plus the timezone case pinning that last N anchors to the supplied zone's midnight.

What survives: the today leg's DST and zone coverage — 7 timezone cells including the 23-hour America/New_York spring-forward day, and 11 utc-window cells — plus the DST cell table's 8 controls.

Each retired case becomes it.todo(...) keeping its original title, so every run's summary still names it. ⛔ This is retirement of pins whose input the contract no longer admits, under ruling batch #57 and the maintainer's option-B decision of 2026-09-07 — not a test disabled to get green.


Generated by Claude Code

…osed dateRange contract no longer admits

Not a test disabled to get green. #16041 closed the string arm of
timeDimensions[].dateRange to the date-range-presets.ts vocabulary
(maintainer ruling, decision batch #57), and this retirement is the
maintainer's option-B decision of 2026-09-07, chosen over routing the
fixtures around the schema door because that would have kept a live pin
on the silent-widening fallback #16041 exists to abolish. The three
memory-analytics-date-range-{dst,timezone,utc-window} files fed the
retired relative dialect ('last 3 days', 'last 7 days', 'last 1 week',
'last 2 weeks', 'last 1 month', 'last 3 months', 'last 1 year') and one
garbage string through asQuery = AnalyticsQuerySchema.parse, so on this
branch they read 15 failed | 37 passed (52) plus three TS2322, on input
production can no longer deliver through any door.

Re-spelling was measured, not assumed: driving the built dist directly,
only 'today' produces a window; every other preset (last_7_days,
last_week, ...) falls to parseDateRangeString's [range, range] fallback
and matches every row, because the parser keys on startsWith('last ').
So 0 of the 15 cases are expressible until #16322 aligns the parser.

Form: it.todo per retired case, original title kept and suffixed with
the reason; a site comment at each naming #16041 and #16322 and stating
the coverage lost; the two surviving helpers' `range` parameter retyped
from string to AnalyticsDateRange (the closed contract), which is what
clears the TS2322. No surviving assertion changed. The dst file's driver
harness (CUBE / asQuery / probesSelected / probesFor) had no surviving
caller and is removed; its cell table and all eight controls survive.

COVERAGE LOST until #16322 reinstates it in preset form:
- dst: all 13 driver cells, i.e. the ENTIRE `last N UNIT` arithmetic-leg
  DST coverage in driver-memory goes to zero: spring-forward and
  fall-back, the day / week / month / year legs, both hemispheres, the
  two non-whole-hour zones (St_Johns -03:30, Chatham +12:45).
- timezone: the one `last N` case anchoring on the SUPPLIED zone's
  calendar day and midnight (Asia/Shanghai, 'last 7 days').
- utc-window: the fallback fence on 'not a range at all'; its
  zone-independence was a property of an answer that matched every row,
  and #16322 owes the driver-side refusal pin in its place.
What survives: the 'today' leg's DST and zone coverage, i.e. the 7
timezone cells including the 23-hour America/New_York spring-forward day
and the 11 utc-window cells, plus every control on the dst cell table.

Reinstatement note for #16322: last_7_days / last_30_days / last_90_days
are rolling day-leg windows; last_week / last_month / last_quarter /
last_year are calendar windows, not n units back, so the week / month /
year cells need re-measured transition instants under preset semantics.

Measured: driver-memory typecheck exit 2 (three TS2322) before, exit 0
after; the three files 15 failed | 37 passed before, 37 passed | 15 todo
after, also under TZ=America/New_York; full package 1095 passed | 15
todo (1110); spec and runtime pins unmoved (235/235, 37/37); dist
byte-identical under a forced rebuild, so the 16 dependents cannot
observe a test-only diff. The same 15 failures are what reddened
Temporal Conformance on d8a3fd2 (its non-SQL leg runs driver-memory
under TZ=America/New_York); the InnoDB line in that job's tail is the
MySQL container's echo of a passing negative test and appears in main's
green run too.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01T6HeZvT9wdSJD1ZxJb5Eno
@github-actions github-actions Bot added size/xl and removed size/l labels Sep 7, 2026

Copy link
Copy Markdown
Collaborator Author

Driver-memory date-range pins retired on this branch — 080052ce83 (maintainer-authorised option B, 2026-09-07)

Not a test disabled to get green. The 15 cases retired in memory-analytics-date-range-{dst,timezone,utc-window}.test.ts fed the relative dialect ('last 3 days', 'last 7 days', 'last 1 week', 'last 2 weeks', 'last 1 month', 'last 3 months', 'last 1 year') and one garbage string through asQuery = AnalyticsQuerySchema.parse — input this PR's narrowing refuses at the schema door, so on this branch they read 15 failed | 37 passed (52) plus three TS2322, on a path production can no longer reach through any door. This is the maintainer's option-B decision (decision batch #57 ruling on the card, option B chosen 2026-09-07 over routing the fixtures around the door, which would have kept a live pin on the silent-widening fallback the card exists to abolish).

Re-spelling was measured, not assumed. Driving the built driver-memory dist directly, only 'today' produces a window; every other preset (last_7_days, last_week, …) falls to parseDateRangeString's [range, range] fallback and matches every row (5/5, 2020 and 2099 included), because the parser keys on startsWith('last '). So 0 of the 15 cases are expressible until #16322 aligns the parser.

Form. it.todo per retired case, original title kept and suffixed with the reason, so every run still lists them by name (37 passed | 15 todo); a site comment at each names #16041 and #16322 and states the coverage lost; the two surviving helpers' range parameter is retyped from string to AnalyticsDateRange, which is what clears the TS2322. No surviving assertion changed. The dst file's driver harness had no surviving caller and is removed; its cell table and all eight controls survive.

Coverage lost until #16322 reinstates it in preset form (the record, also in the commit message):

Measured at 080052ce83 (under os-verify-lock): pnpm --filter @objectstack/driver-memory typecheck exit 2 (three TS2322) before, exit 0 after; the three files 15 failed | 37 passed before, 37 passed | 15 todo (52) after, also under TZ=America/New_York; full package 44 files, 1095 passed | 15 todo (1110); spec pins 235/235 and runtime pins 37/37 unmoved; driver-memory dist/ byte-identical under a forced rebuild (three git hash-object hashes equal), so the 16 dependents turbo ls --affected lists cannot observe a test-only diff; eslint on the three files, check:nul-bytes, check:cross-package-test-inputs, check:test-source-alias, check:type-check-coverage all exit 0.

The three reds on d8a3fd2d08 are one defect. Type Check · workspace failed only on @objectstack/driver-memory#typecheck exited (2); Test Core (2/6) failed only on @objectstack/driver-memory#test (15 failed | 1095 passed); and Temporal Conformance (live PG + MySQL) failed at step 14, Run the non-SQL temporal backends under the skewed process zone, on the same 15 driver-memory failures under TZ=America/New_York — step 12, the driver-sql leg, concluded success. The InnoDB [MY-011825] … os11565_narrow … line in that job's tail sits in the post-job MySQL service-container dump and is the server's echo of the passing negative test reports InnoDB page-limit refusals with the page limit (sql-driver-11565-row-byte-budget.test.ts); the identical line appears in main's green Temporal run on 8341ed21ce. Not driver-sql, not a flake; this commit is the repair.

Still draft; nothing armed.


Generated by Claude Code

@huangyiirene
huangyiirene marked this pull request as ready for review September 7, 2026 15:30

Copy link
Copy Markdown
Collaborator Author

Green and armed — the four reds were one defect, and one commit proved it

All 38 check runs read on 080052ce83 (reconciled against the reported total, not a first page), every one success or skipped. Every previously-red check is now green:

check on d8a3fd2d08 on 080052ce83
TypeScript Type Check failure success
Type Check · workspace failure success
Test Core (2/6) failure success
Temporal Conformance (live PG + MySQL) failure success

⭐ That last row is the one that settles the diagnosis. I had twice called Temporal Conformance a driver-sql failure "not this PR's", reading the job log from its tail. The dev read get_workflow_job step conclusions instead and showed step 14 (the non-SQL leg, TZ=America/New_York) carried the same 15 driver-memory failures, while step 12 (driver-sql) was success — and that the InnoDB os11565_narrow line is a passing negative test's server echo, present in main's green runs too. A single test-only commit turned all four green, which is the confirmation that reading was right and mine was wrong.

What landed

080052ce83 — 3 files, +79/−144, test files only. parseDateRangeString, the strategy and every non-test file untouched.

Each retired case is it.todo(...) keeping its original title, so every run's summary still names it, with a site comment citing #16041 and #16322. Two helper range parameters retyped stringAnalyticsDateRange — that is what greens the three TS2322. No surviving assertion changed.

This is retirement of pins whose input the contract no longer admits — under ruling batch #57 and the maintainer's option-B decision of 2026-09-07 — not a test disabled to get green. The commit message carries that distinction and the coverage loss, so git history holds it without this thread.

Re-spelling was refused by measurement, not argument: driving the built dist directly, only today yields a window; all twelve other presets fall to the [name, name] fallback and match every row including 2099. 0 of 15 cases were expressible.

Coverage given up, and where it comes back

Gone until #16322: the entire last N UNIT arithmetic-leg DST coverage in driver-memory — 13 cells across both hemispheres, day/week/month/year legs, including St_Johns (−03:30) and Chatham (+12:45) — plus the case pinning that last N anchors to the supplied zone's midnight.

Surviving: the today leg's DST and zone coverage — 7 timezone cells including the 23-hour America/New_York spring-forward day, 11 utc-window cells, and the DST cell table's 8 controls.

The concrete reinstatement requirement is on #16322 (comment 5572379368), including the trap that last_week/last_month/last_quarter/last_year are calendar windows rather than n units back, so those cells need re-measured transition instants rather than a mechanical re-spelling.

⚠️ Also recorded there, and it raises that card's urgency: once this merges, a valid preset such as last_30_days is accepted by the schema and then silently widens in driver-memory — the same defect class this card abolishes, relocated onto the blessed vocabulary. Off the production SQL path (the parser is driver-memory-local), which is why it does not block here, but it is the strongest argument yet for doing #16322 next.

needs:contract-review deliberately left on so CI's LEVEL AXIS judges the grade rather than reporting not-measured — the contract review itself passed at 5571543935. Ready flipped, auto-merge on.


Generated by Claude Code

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

Projects

None yet

2 participants