Repository navigation
fix(spec): the one-day date-range presets prescribe a one-day window, and the table states its end-token convention - #17338
Conversation
`DATE_RANGE_PRESET_MACRO_WINDOWS` maps each dashboard date-range preset to the
`{date-macro}` window a refusal PRESCRIBES to an author who wrote the preset
name as a bare filter comparand. Two of its thirteen entries prescribed a
window wider than the preset they name.
`yesterday` spelled its end as `{today}` — the day AFTER the window. The pair
is written for `$between`, which is `$gte min` and `$lte max`, and a bare-day
upper bound means "through that whole day", compiled half-open to
`< nextUtcCalendarDay(max)` (ADR-0053 D-D). So the prescription resolved to
`>= yesterday 00:00 AND < tomorrow 00:00` — yesterday AND today, two days for
a one-day preset.
`today` took the open `$gte`-only arm, so its prescription had no upper bound
at all and selected every day after today on a column carrying future dates.
This package's own conformance corpus already measures the gap: with the
fixture clock on 2026-07-28, `$between ['{today}', '{today}']` selects
c_open/d_mid/e_late and is annotated "the today preset degenerates to a single
day", while `$gte '{today}'` additionally selects f_next and g_eom.
Both entries now name their own last day, matching the convention the other
eight closed entries already used and matching both executable mappings —
objectui's `PRESET_RANGES` and `@objectstack/core`'s analytics resolver.
The convention itself was nowhere written down, which is what let one table
carry two readings. It is now stated as a rule on the table: start names the
window's first calendar day, end names its last, inclusive, never the day it
stops before; `end: null` is the open arm and is exactly the three rolling
`last_N_days` windows. Tests pin the resolved extent of every window against a
frozen reference day and require a stated extent for every declared preset, so
a preset added later cannot silently pick the other reading.
Claude-Session: https://claude.ai/code/session_01MkQhmuuJAVDjmeWNixwDDH
Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MkQhmuuJAVDjmeWNixwDDH Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift Check
What this run could not see
Coarse fallback — 134 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): |
ACCEPT — head
|
Fixes #17014
DATE_RANGE_PRESET_MACRO_WINDOWSmaps each dashboard date-range preset to the{date-macro}window a refusal prescribes to an author who wrote the preset name as a bare filter comparand. Two of its thirteen entries prescribed a window wider than the preset they name.The real work here was the convention question, not the token edit: the table carried two readings of what an
endtoken means and stated neither, so the outlier was invisible to every gate and to the next author. The convention is now written on the table and pinned by tests.Reproduction, verbatim
Both runs read the built
dist(pnpm --filter @objectstack/spec build, exit 0 both times) and print the table entries plus the window each rendered refusal quotes back.Before — at
47863f4fb(origin/mainat branch point; the card's own run was at419facdde, re-measured here):After — this branch:
The mechanism, re-measured at head
The card's premise held.
$betweenis$gte minand$lte max, and a bare-day upper bound means "through that whole day", compiled half-open to< nextUtcCalendarDay(max). That rule is live and is stated inside this same directory —packages/spec/src/data/calendar-day.ts(ADR-0053 D-D), whose own table reads:So
['{yesterday}', '{today}']prescribed>= yesterday 00:00 AND < tomorrow 00:00— yesterday and today, two days for a one-day preset.Census of all thirteen entries against the convention
Read entry by entry, not grepped for a shape. Each
endtoken's denotation is quoted fromDATE_MACRO_DESCRIPTIONSindate-macros.zod.ts.todaynull, now{today}yesterday{today}, now{yesterday}this_week{week_end}last_week{last_week_end}this_month{month_end}last_month{last_month_end}this_quarter{quarter_end}last_quarter{last_quarter_end}this_year{year_end}last_year{last_year_end}last_7_daysnulllast_30_daysnulllast_90_daysnull10 closed + 3 open = 13, the whole table.
The second outlier, and the measurement that put it in scope
The card and the triage comment both read
todayas "takes the$gte-only arm and is unaffected". That premise did not survive re-measurement. Three independent readings say atodaypreset is a closed one-day window:packages/spec/src/data/temporal-conformance.tsstates the case{ at: { $between: ['{today}', '{today}'] } }, annotated "The 'today' preset degenerates to a single day", expecting rowsc_open,d_mid,e_late. Two cases below,{ at: { $gte: '{today}' } }— which is exactly what the old entry prescribed — expectsc_open,d_mid,e_late,f_next,g_eom.f_nextis stamped the next midnight andg_eomthree days later. The corpus therefore already measures the extra rows the prescription hands an author.packages/core/src/utils/dashboard-filters.tshastoday: { from: '{today}', to: '{today}' }andyesterday: { from: '{yesterday}', to: '{yesterday}' }. All thirteen of its rows are closed.@objectstack/core,analytics-date-range.ts) resolvestodayto['today', 'tomorrow']in stop-before spelling — one day — and reserves its ownnullarm for the three rolling presets only.The old
todayentry's blast radius is larger thanyesterday's, not smaller:$gtealone has no upper bound, so on any column carrying future dates (aclose_date, a scheduled-at) the prescription selects every day from today onward. Its own TSDoc justified thenullarm only for "the rollinglast_N_dayswindows";todaywas riding along unstated. Closing it restores that sentence to exactly the set it describes.The convention, and where it is now written down
Written as a rule in the TSDoc of
DATE_RANGE_PRESET_MACRO_WINDOWSitself,packages/spec/src/data/date-range-presets.ts:with the reason (
$betweenplus the ADR-0053 D-D bare-day rule), an explicit ban on the other convention, a pointer to where that other convention legitimately lives, and the rule thatend: nullis the open arm reserved for exactly the three rolling windows.A type-level fence was considered and declined, with the reason recorded here rather than silently. The end tokens carry no shared spelling to constrain: eight are
*_endforms, but the corrected single-day ends are{today}and{yesterday}, which are instant tokens. A template-literal type over_endwould reject the very values this PR proves correct. What decides the question is what a token denotes, which lives inDATE_MACRO_DESCRIPTIONS, not in its spelling — so the fence is a test, and it is built to be one a new preset cannot walk past.Tests
packages/spec/src/data/date-range-presets.test.tsgains five cases underthe prescribed window covers exactly the days the preset names. They avoid restating the table: one local table says what each token denotes on a frozen Wednesday (2026-07-15), a second says what each preset name denotes, and the pins ask whether the shipped table joins the two — computing the resolved extent through the realnextUtcCalendarDayfrom./calendar-day.ts.last_N_dayswindows.Reverse verification (ablation)
The pre-fix values were restored on disk, the suite re-run, and the tree restored — a one-off proof, not a committed file.
On-disk proof of the mutation, before the run: injected
['{today}', null]read 1, removed['{today}', '{today}']read 0, and the file's blob hash differed from itsHEADblob.Result —
4 failed | 7 passed (11), exactly the four new cases, with the extent pin naming the defect as a count:Restore proof:
git diff HEADempty, andgit hash-objecton the file equal to itsHEADblob again.Gates
Derived from the real change set rather than from a remembered list —
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack, re-derived after the changeset existed (it named six families that only apply once one does).Ran, green — 77 of the 81 derived commands. Every exit code was redirected to a file and read back from disk, never through a pipe. On top of them:
pnpm --filter @objectstack/spec check:generated— exit 0, "All 15 generated artifacts are up to date". Nothing needed regenerating:check:docs,check:api-surface,check:authorable-surface,check:export-origins,check:declaration-map,check:migration-registry,check:spec-changesandcheck:upgrade-guideare all current, which is the measured form of "no schema or export moved".pnpm --filter @objectstack/spec test— 471 files / 13256 tests passed.pnpm --filter @objectstack/spec typecheck— exit 0, and its test layer really does reach the new cases rather than being assumed to:tsc -p tsconfig.test.json --listFileslistssrc/data/date-range-presets.test.ts(1 hit, among 442 test files; a fabricated path reads 0).pnpm --filter @objectstack/lint exec vitest run src/validate-preset-comparands.test.ts— 17 passed. Thefilter-preset-comparandvocabulary consumer: it constrains nothing about this fix, because every window literal it asserts islast_30_days/{30_days_ago}— it usesyesterdayonly as an offending comparand, never as an expected window.pnpm --filter @objectstack/core exec vitest run src/utils/analytics-date-range.test.ts— 32 passed. This is the suite that pins the STARTS of this table against the resolver's; starts are untouched.pnpm exec eslint . --no-inline-config --format json— the whole-repo scan, run after the final commit atec57a80796: exit 0 over 6481 files, 0 errors, 0 warnings. No narrowing was needed, so none is claimed. (eslint.config.mjsenables no type-aware linting — noparserOptions.project, no typed rules — which is also why this diff could not have moved a verdict on a file it does not touch.)NOT MEASURED — 4. Every one exited 3, the code these gates reserve for
PREREQUISITE NOT MET, in a worktree where only@objectstack/specis built. ⛔ None of them is a finding, and none is red:pnpm --filter @objectstack/lint run check:doc-formula-expressions@objectstack/formulais not built"pnpm check:dual-build-cjs-loadspnpm check:lean-entry-closurepnpm check:type-check-debt--re-measurehalf — "31 workspace dependencies have no built type entry point on disk". Its coverage half printedOK — 76/80 workspace packages type-checked, andpnpm check:type-check-coverageran separately and is green.Clearing those four needs the whole workspace built (
turbo run build --filter='./packages/*'), which is CI's run on this PR, not this worktree's. This narrowing is declared, not silent.The derivation tool also names, as outside its own 81, what no local invocation can cover: 46 artifact-roster families, 11 declared wide-population families, 5 families taking a value from the workflow, and 5 path-scheduled CI jobs (Test Core, Build Core, Temporal Conformance, and the two Dogfood jobs). Those are CI's.
Clause ②
Clause-②: no— measured, not assumed, and it agrees with the seat's reading.check:api-surface,check:export-originsandcheck:authorable-surfaceare green with no regeneration. The exported const keeps its declared type; only two of its values change.@objectstack/lint'svalidate-preset-comparandsandpackages/spec'sfilter.zod.tsboth callbareDateRangePresetComparandMessage; neither pins a window literal fortodayoryesterday— the lint suite's message assertions are all onlast_30_daysand its{30_days_ago}window, and its own fixtures useyesterdayonly as an offending comparand, never as an expected window.@objectstack/core'sanalytics-date-rangepins that the window STARTS agree with this table; starts are untouched.{yesterday}appears in only four places outsidedist, and exactly one of them was the entry this PR edits.Nothing is narrowed and nothing is widened: the set of shapes the refusal fires on is byte-identical, and only the window text a refusal quotes back moves.
Acceptance notes — noted, not filed
packages/core/src/utils/analytics-date-range.tsexplains why it states its ends itself and ends with "so its ends are one day earlier for the eight period presets". After this PR the relationship holds for all ten calendar presets, so that enumeration now reads as "and the other two agree", which is no longer true. It is a comment-only correction and the file sits inside this card's declared do-not-touch fence (the Analytics drivers (memory, SQL) aligndateRangestring handling to the closed preset vocabulary (driver half of #16041) #16322 resolver), so it is left for the reviewingdomain:specseat, which is the named carrier and reads that file as part of this review.Out-of-scope finding — filed
packages/spec/src/data/date-macros.zod.ts's module header still describes the pre-ADR-0053 upper-bound semantics: it says a<=against a*_endtoken "stops at midnight on the 31st" and tells the author to reach for a half-open< {next_year_start}instead.calendar-day.ts, one file over, states the opposite as the platform rule. Filed rather than folded in: it is a separate contract-text defect, and correcting a module header on a file of Zod schemas pulls in thecheck:docs/gen:schemaregeneration surface this PR does not otherwise touch. Filed as spec:date-macros.zod.ts's header still states the pre-ADR-0053 upper-bound rule, so it tells an author a*_endtoken stops at midnight #17333 (no labels, no assignee, per the filing rule).Generated by Claude Code
Generated by Claude Code
Generated by Claude Code