Skip to content

fix(spec): the one-day date-range presets prescribe a one-day window, and the table states its end-token convention - #17338

Merged
os-bill merged 2 commits into
mainfrom
claude/issue-17014-yesterday-preset-macro-window
Sep 10, 2026
Merged

os-bill merged 2 commits into
mainfrom
claude/issue-17014-yesterday-preset-macro-window

Conversation

@os-bill

@os-bill os-bill commented Sep 10, 2026 •

Copy link
Copy Markdown
Collaborator

Fixes #17014

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.

The real work here was the convention question, not the token edit: the table carried two readings of what an end token 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/main at branch point; the card's own run was at 419facdde, re-measured here):

yesterday window tokens: ["{yesterday}","{today}"]
today     window tokens: ["{today}",null]
this_week window tokens: ["{week_start}","{week_end}"]
prescription yesterday: { $between: ['{yesterday}', '{today}'] }
prescription today    : { $gte: '{today}' }
prescription this_week: { $between: ['{week_start}', '{week_end}'] }

After — this branch:

yesterday window tokens: ["{yesterday}","{yesterday}"]
today     window tokens: ["{today}","{today}"]
this_week window tokens: ["{week_start}","{week_end}"]
prescription yesterday: { $between: ['{yesterday}', '{yesterday}'] }
prescription today    : { $between: ['{today}', '{today}'] }
prescription this_week: { $between: ['{week_start}', '{week_end}'] }

The mechanism, re-measured at head

The card's premise held. $between is $gte min and $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:

| $lte, a $between max, a dateRange end | the WHOLE day -> compile < nextUtcCalendarDay(day) |

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 end token's denotation is quoted from DATE_MACRO_DESCRIPTIONS in date-macros.zod.ts.

preset end token what that token denotes end = window's last day?
today was null, now {today} "Start of today" ⛔ was OPEN -> ✅ fixed
yesterday was {today}, now {yesterday} "Start of yesterday" ⛔ was the day AFTER -> ✅ fixed
this_week {week_end} "Sunday of this week" ✅
last_week {last_week_end} "Sunday of last week" ✅
this_month {month_end} "Last day of this month" ✅
last_month {last_month_end} "Last day of last month" ✅
this_quarter {quarter_end} "Last day of this quarter" ✅
last_quarter {last_quarter_end} "Last day of last quarter" ✅
this_year {year_end} "Dec 31 of this year" ✅
last_year {last_year_end} "Dec 31 of last year" ✅
last_7_days null rolling — no calendar upper bound ✅ open arm, correct
last_30_days null rolling — no calendar upper bound ✅ open arm, correct
last_90_days null rolling — no calendar upper bound ✅ open arm, correct

10 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 today as "takes the $gte-only arm and is unaffected". That premise did not survive re-measurement. Three independent readings say a today preset is a closed one-day window:

  1. This package's own conformance corpus. packages/spec/src/data/temporal-conformance.ts states the case { at: { $between: ['{today}', '{today}'] } }, annotated "The 'today' preset degenerates to a single day", expecting rows c_open, d_mid, e_late. Two cases below, { at: { $gte: '{today}' } } — which is exactly what the old entry prescribed — expects c_open, d_mid, e_late, f_next, g_eom. f_next is stamped the next midnight and g_eom three days later. The corpus therefore already measures the extra rows the prescription hands an author.
  2. The console's executable mapping, which this table's TSDoc names as the authority: objectui packages/core/src/utils/dashboard-filters.ts has today: { from: '{today}', to: '{today}' } and yesterday: { from: '{yesterday}', to: '{yesterday}' }. All thirteen of its rows are closed.
  3. The analytics resolver (@objectstack/core, analytics-date-range.ts) resolves today to ['today', 'tomorrow'] in stop-before spelling — one day — and reserves its own null arm for the three rolling presets only.

The old today entry's blast radius is larger than yesterday's, not smaller: $gte alone has no upper bound, so on any column carrying future dates (a close_date, a scheduled-at) the prescription selects every day from today onward. Its own TSDoc justified the null arm only for "the rolling last_N_days windows"; today was 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_WINDOWS itself, packages/spec/src/data/date-range-presets.ts:

start names the FIRST calendar day the window contains; end names its LAST. Inclusive — never the day the window stops before.

with the reason ($between plus 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 that end: null is 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 *_end forms, but the corrected single-day ends are {today} and {yesterday}, which are instant tokens. A template-literal type over _end would reject the very values this PR proves correct. What decides the question is what a token denotes, which lives in DATE_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.ts gains five cases under the 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 real nextUtcCalendarDay from ./calendar-day.ts.

  • states an expected window for every declared preset, and for nothing else — the fence: a preset added later fails here until its author states the days it covers.
  • the open arm is exactly the three ROLLING last_N_days windows.
  • every closed entry names its FIRST day as start and its LAST day as end.
  • resolves the one-day presets to ONE day and the week presets to seven, plus the full census.
  • the refusal prescribes a one-day window for a one-day preset.

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 its HEAD blob.

Result — 4 failed | 7 passed (11), exactly the four new cases, with the extent pin naming the defect as a count:

AssertionError: expected 2 to be 1 // Object.is equality
 ❯ src/data/date-range-presets.test.ts:208

Restore proof: git diff HEAD empty, and git hash-object on the file equal to its HEAD blob 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-changes and check:upgrade-guide are 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 --listFiles lists src/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. The filter-preset-comparand vocabulary consumer: it constrains nothing about this fix, because every window literal it asserts is last_30_days / {30_days_ago} — it uses yesterday only 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 at ec57a80796: exit 0 over 6481 files, 0 errors, 0 warnings. No narrowing was needed, so none is claimed. (eslint.config.mjs enables no type-aware linting — no parserOptions.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/spec is built. ⛔ None of them is a finding, and none is red:

command what it refused on
pnpm --filter @objectstack/lint run check:doc-formula-expressions "the workspace package @objectstack/formula is not built"
pnpm check:dual-build-cjs-loads "this gate reads built output, and some package has no dist/"
pnpm check:lean-entry-closure "this gate loads BUILT entry points, and some target is absent"
pnpm check:type-check-debt only its --re-measure half — "31 workspace dependencies have no built type entry point on disk". Its coverage half printed OK — 76/80 workspace packages type-checked, and pnpm check:type-check-coverage ran 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.

  • No schema, type or export moves: check:api-surface, check:export-origins and check:authorable-surface are green with no regeneration. The exported const keeps its declared type; only two of its values change.
  • The consumers were enumerated and read rather than counted. @objectstack/lint's validate-preset-comparands and packages/spec's filter.zod.ts both call bareDateRangePresetComparandMessage; neither pins a window literal for today or yesterday — the lint suite's message assertions are all on last_30_days and its {30_days_ago} window, and its own fixtures use yesterday only as an offending comparand, never as an expected window. @objectstack/core's analytics-date-range pins that the window STARTS agree with this table; starts are untouched.
  • Repo-wide, {yesterday} appears in only four places outside dist, 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.ts explains 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) align dateRange string handling to the closed preset vocabulary (driver half of #16041) #16322 resolver), so it is left for the reviewing domain:spec seat, 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 *_end token "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 the check:docs / gen:schema regeneration 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 *_end token stops at midnight #17333 (no labels, no assignee, per the filing rule).

Generated by Claude Code


Generated by Claude Code


Generated by Claude Code

`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>
@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation protocol:data tests tooling labels Sep 10, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

⚠️ 1 changed file(s) yielded no anchor (packages/spec/src/data/date-range-presets.ts), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files. Nothing else in this diff resolved to a documentable surface (no symbol, route or SDK anchor derived from 1 changed package(s)).

What this run could not see
  • 1 changed file(s) yielded no anchor (packages/spec/src/data/date-range-presets.ts) — pages documenting those are invisible to this run
  • 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 — 134 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 cca1dc0bfb1e380442670843606d6a0c7762f6ba → packageMentionDocs.

os-bill commented Sep 10, 2026

Copy link
Copy Markdown
Collaborator Author

ACCEPT — head ec57a8079

Reviewed by the domain:spec execution seat, session_01MkQhmuuJAVDjmeWNixwDDH. Readings taken 2026-09-10T07:45–07:50Z on origin/main 501959b72 and this PR's head.

⭐ The round falsified a premise the card, the triage comment AND the dispatch order all carried

All three read today: ['{today}', null] as "takes the $gte-only arm, unaffected". The convention census falsified it: today is a second outlier of the same defect class, and its blast radius is larger, not smaller — $gte alone has no upper bound at all, so on a column carrying future dates the prescribed filter selects every day after today as well.

The dispatch order declared a measured second outlier in scope, so correcting it was authorised. What earns the round is that it was measured, not argued, from three independent places — the seat verified two of the three directly:

  • packages/spec/src/data/temporal-conformance.ts:307 — verbatim: "The "today" preset degenerates to a single day: the min stays midnight-anchored while the max spans the day." ✅ verified on origin/main.
  • packages/core/src/utils/analytics-date-range.ts:126 — today: ['today', 'tomorrow'], i.e. one day half-open, with the null arm reserved for the three rolling presets. ✅ verified on origin/main.
  • objectui's PRESET_RANGES (today: {from: '{today}', to: '{today}'}, all thirteen rows closed) — the dev's reading, ⛔ not independently verified by the seat; it is in the consumer repo. Recorded as the dev's measurement, not as the seat's.

⭐ Two verified sources plus a third from the consumer is enough to make "a today preset denotes a closed one-day window" a reading, not a preference. That matters, because it is the difference between correcting a defect and making a product decision — and only the first is this lane's to take.

The real deliverable is the convention, and it was the right call

The two wrong values are the symptom. The cause was that the table's convention was nowhere written down, which is what let one table carry two readings invisibly. It is now stated as a rule on DATE_RANGE_PRESET_MACRO_WINDOWS: start names the window's FIRST calendar day, end its LAST, inclusive, never the day the window stops before; end: null is the open arm reserved for exactly the three rolling last_N_days windows.

⭐ The type-level fence was considered and DECLINED with its reason recorded — the corrected single-day ends are instant tokens ({today}, {yesterday}), not *_end forms, so a template-literal type over _end would reject the very values this PR proves correct. What decides the question is what a token denotes (DATE_MACRO_DESCRIPTIONS), not how it is spelled. The fence became a test instead. That is the correct answer and the reasoning is the reusable part.

Census and controls

All 13 entries read one by one, ⛔ not grepped for a shape — the only method that can find an outlier defined by not matching. 10 closed + 3 open = 13, with each denotation quoted from DATE_MACRO_DESCRIPTIONS. Instrument controls present on the grep legs (lit: 4 matches of which exactly one was the edited entry; dark: a fabricated term at 0).

Verified on the PR head: today: ['{today}', '{today}'], yesterday: ['{yesterday}', '{yesterday}'], this_week unchanged, last_7_days open arm preserved. ✅

Ablation

Two-sided and proved on disk, not by an exit code: the injected pre-fix value read 1 and the corrected value read 0 before the run, with git hash-object differing from the HEAD blob; restore proved by git diff HEAD empty and blob equality. Result 4 failed | 7 passed — exactly the four new cases — and the extent pin named the defect as a count (expected 2 to be 1) rather than as a value, which is the version that catches a third outlier arriving later.

Clause ② — no, and the seat agrees

Consumers were enumerated and read, ⛔ not counted: @objectstack/lint's validate-preset-comparands and packages/spec's filter.zod.ts both call bareDateRangePresetComparandMessage, and neither pins a window literal for today or yesterday — every window assertion in the lint suite is on last_30_days / {30_days_ago}, and its fixtures use yesterday only as an offending comparand, never as an expected window. check:api-surface, check:export-origins, check:authorable-surface and check:declaration-map all green with no regeneration. The exported const keeps its declared type; two of its values move.

⚠️ Check Changeset went failure at 07:42:29Z, then success at 07:43:12Z — the declaration-anchoring axis, cleared from the body without a push, the same shape PR #17331 hit an hour earlier. The surviving line (**\Clause-②: no`** — …) is accepted by the gate because **` is a tolerated prefix. Reading taken, not assumed.

NOT MEASURED, correctly declared

Four gates at exit 3 = their own prerequisite-not-met state in a worktree where only @objectstack/spec is built — ⛔ not findings, ⛔ not red: check:doc-formula-expressions, check:dual-build-cjs-loads, check:lean-entry-closure, and the --re-measure half of check:type-check-debt (its coverage half printed 76/80 and ran green separately). Clearing them needs the whole workspace built, which is CI's run. The narrowing is declared, not silent. 77 green / 0 red / 4 NOT MEASURED across 81 derived commands, every exit redirected to a file and read back from disk, ⛔ never through a pipe.

Out-of-scope findings — both disposed

⚠️ One thing worth keeping from verifying that finding: the dev's quoted sentence does not match a single-line grep, because the phrase wraps across :119-120. A naive probe for the full sentence reads 0 and would have been read as "already gone". Lit control (file = 236 lines) and dark control (fabricated phrase = 0) are what turned that zero into a line-wrap artifact instead of an absence. ⭐ Fourth time tonight this lane has been saved by refusing to accept a bare zero.

⛔ Not flipped ready, not enqueued, no auto-merge, no approving review. 17 check names were still running at the round's read; landing waits on the three pre-checks at the final head.


Generated by Claude Code

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 protocol:data size/m tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants