Skip to content

feat(verify): the dashboard date-window family, runnable as os verify --dashboard-windows (#15953 slice 5) - #23150

Draft
objectstack-fleet[bot] wants to merge 7 commits into
mainfrom
claude/issue-15953-s5-dashboard-window
Draft

objectstack-fleet[bot] wants to merge 7 commits into
mainfrom
claude/issue-15953-s5-dashboard-window

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Part of #15953
Clause-②: yes

What this is

Slice 5 of #15953: the D residue of the classification table's row 9 (6115219467), "every datetime-windowed dashboard answers under its own picker". hotcrm's test/dashboard-date-range-window.test.ts describe 2 pins that by hand for its Service dashboard. It is now a derived proof family in @objectstack/verify (the seventh, beside runCrudVerification), runnable as the opt-in os verify --dashboard-windows. Row 9's R half (describe 1, the datetime filter window per storage shape) stays where the table puts it, in the temporal-conformance matrix upstream; it is not in this PR. The card stays open for its remaining rows.

The family (packages/verify/src/dashboard-window.ts)

The property. Every dashboard that declares a dateRange answers, under each preset of its own picker, with the rows that window selects. Per bound widget and count measure, three verdicts per preset: the widget is not blank where the window holds fixture rows; its count equals the rows the window selects; and a narrower preset never answers more than a wider one. A window that never narrows is named inert.

Where each expectation is read from (published sources only, nothing re-derived):

  • The binding: DashboardWidgetSchema.filterBindings (packages/spec/src/ui/dashboard.zod.ts). A string binds the window to that field, false opts the widget out (unbound), and absent means the filter's own field, "dateRange: dateRange.field ?? 'created_at'". Dashboards and datasets are read through DashboardSchema.safeParse and DatasetSchema.safeParse. The field resolves against dataset.object as the booted stack registers it, so created_at (a registry-injected column) resolves. It must be date or datetime. The created_at literal has no export; it is named once with its source, beside the lint's own copy (DATE_RANGE_DEFAULT_FIELD in validate-widget-bindings.ts).
  • A preset's window: DATE_RANGE_PRESET_MACRO_WINDOWS (@objectstack/spec/data), for each of the 13 DATE_RANGE_PRESETS. It is handed as written, { $gte: start, $lte: end } (end absent for the three rolling presets), ANDed with the widget's own filter as { $and: [filter, window] }, the shipped console's merge (objectui mergeFilters). The server resolves the tokens (dataset-executor.ts resolveSelectionTokens), and the verifier lowers nothing.
  • The ground truth's day boundaries: the same window object, resolved by @objectstack/core's resolveFilterTokens (the resolver the executor calls) on the caller's ExecutionContext.timezone, UTC when unset. Membership follows the table's own stated convention: first and last day inclusive, rolling end open.

The door. POST {basePath}/analytics/dataset/query with datasetName and the widget's own selection (its dimensions, one count measure, its filter with the window ANDed in as runtimeFilter), as the signed-in principal the caller passes in. os verify passes the dev admin from signIn(). Measured on bootStack at 21c86444ec:

hotcrm's file called AnalyticsService.queryDataset in process as { isSystem: true, userId: VIEWER }; the family uses the HTTP door every dashboard request takes. {current_user_id} in a widget filter is that principal, and the server resolves it.

Fixtures.

  • Days. One row per day across every preset's first and last day and the day either side, never after today: 20 days on 2026-10-13, from 2024-12-31 to today. Each row is stamped at UTC noon; a date field takes the day itself.
  • Door and shape. Rows go through the seed door, which measurably keeps an explicit created_at. They are the smallest CRUD-derived record; if the seed door refuses that, every field the CRUD derivation synthesizes is used instead, and the report names which shape it took.
  • Owner and held columns. Each row is owned by the principal (seeded rows carry no owner otherwise). Every column the widget's filter, the dataset's filter and the measure's filter compare is held to a value they select. Modelled: an equality, $in (its first member), $nin or $ne on a field that declares options (the first option not excluded, so no row is NULL there), {current_user_id}, and $and of these. Anything else is an exclusion with its reason, never a guess, as hotcrm's truthMatches throws.
  • Read-back. Every row is read back before anything is judged. A stamp or held column that does not read back as written (an app hook rewrote it) makes the probe not run, never contradicted. So does a column the read omits (internal: true), named as unconfirmed rather than rewritten.

The comparison. Every count is a difference: the count after the fixtures minus the count before, under the same window, so the app's own rows cancel. The control leg is the widget with no window: it must count every fixture row, or the fixtures do not meet the widget's own selection and no window is judged. A count of a named field gets a non-null value held. An average, a sum or a derived measure is excluded and counted.

Statuses. proven, contradicted (blank, or another count), failed (the windowed query refused or faulted while the unwindowed one answered), inconclusive, unbound, excluded (with excludedKind). hardFailures = contradicted + failed widgets.

Controls and ablations

  • NEGATIVE CONTROL, drop (dashboard-window.test.ts): the app's own plugin registers an aggregate middleware on the object, through the engine's published seam (registerMiddleware, ctx.operation === 'aggregate', ctx.ast.where, the seam the engine's own refusal text prescribes for read filters). It removes the window's condition, so the floor matches every row, the [17.0.0-rc.0] SQLite datetime window filters return empty: filter comparands coerced to epoch-ms while writes store ISO TEXT #3912 class. Found: every preset contradicted ("the window counted 20 fixture row(s), and it selects N (wider)"), named inert, hardFailures 1. No engine source was changed.
  • NEGATIVE CONTROL, zero: the same seam, making a windowed query match nothing. Found: every preset blank.
  • ATTRIBUTION CONTROL: an app hook stamping the window's field on insert reads back as rewritten, so the probe does not run and the widget is inconclusive, never contradicted. An internal: true window field reads back "no audited_at (the read does not return that field)", unconfirmed.
  • CLI: packages/cli/test/verify-dashboard-windows.test.ts spawns os verify. Without the flag there is no dashboardWindows key and exit 0. With it, the widget is proven under 13 presets and the run exits 0. On the same app plus the drop plugin, the widget is contradicted and inert, hardFailures 1, exit 1.
  • ABLATIONS of the family's own comparison (judge): scripts/ablation-replace.mjs, inside a scratch script with trap restore EXIT INT TERM on absolute paths, at 3228b5954b. No build is involved: the test imports the module by a relative path.
    • A: if (answered === expected) return { outcome: 'held', ... } becomes an unconditional held plus ABLATION_15953_S5_A. Anchor 1 → 0, marker 0 → 1, blob 36251c04 → ff99b96d. RED: Tests 2 failed | 8 passed (10), the drop control and its summary line, as predicted. Restored: blob 36251c04 equals HEAD, git diff HEAD empty.
    • B: the blank branch becomes if (false) plus ABLATION_15953_S5_B. Anchor 1 → 0, blob 36251c04 → 58ef1cfa. RED: Tests 1 failed | 9 passed (10), exactly the zero control, as predicted. Restored to HEAD's blob, empty diff.

First consumer: hotcrm

hotcrm read-only clone, fetched: main at 0f84c9c7. The test file, every dashboard and every dataset are unchanged since 1d7148bf; case.object.ts changed a comment only, and account.object.ts lost one select defaultValue that no windowed widget reads. The runs use slice 3's artifact at 1d7148bf (built by hotcrm's own CLI 17.7.0) and its os migrate meta --from 17 snapshot. engines.protocol was moved to ^18 and the 76 email_template.category tombstones were removed in memory, as slices 3-4 did. Nothing was written to hotcrm. os verify still cannot boot hotcrm (protocol ^17.7.0 against this tree's 18), so the family ran directly over the artifact.

Population: 5 dashboards, 4 windowed. Those 4 carry 49 widgets: 21 unbound, 13 excluded (all no-count-measure: sums, averages and derived ratios), and 15 probed. All 15 probed widgets are count widgets over crm_case.created_at (datetime) and crm_opportunity.close_date (date); 5 non-count measures beside them are excluded. That makes 195 windows per run.

run engine hooks proven contradicted inconclusive windows held / not run
C this tree without bodies 15 0 0 195 / 0
B this tree the artifact's 44 bodies grafted 12 0 3 156 / 39
A hotcrm's pinned @objectstack/verify 17.7.0 bootStack, the artifact as built live 12 0 3 156 / 39

In every run: 0 blank, 0 inert, 0 narrowing violations. The 3 inconclusives in A and B are kb_resolved_cases, closed_cases_total and top_resolving_articles: hotcrm's own hook derives is_closed from status, so a held is_closed: true reads back false. The attribution control is doing its job on a real app. won_deals meets hotcrm's "Win Reason is required" rule only with the every-field shape.

Against describe 2 (service_dashboard, run C; runs A and B agree wherever their fixtures read back):

  • "is exercising at least one dashboard": service_dashboard is windowed on created_at, and so are 3 more dashboards the file never covered (on close_date, a date field).
  • "leaves no widget blank at last_7/30/90_days": 0 blank over all 13 presets for its 10 bound count widgets. The file's blank check also covered three non-count widgets (avg_resolution_time, sla_compliance_gauge, kb_deflection_rate); the family excludes those and counts them.
  • "counts exactly the rows the window selects": every case_count widget the file compared holds under all 13 presets: open_cases, critical_cases, sla_violations, cases_by_status, cases_by_priority, cases_by_origin, my_open_cases_by_priority (its {current_user_id} filter is the principal). So do closed_cases_total, kb_resolved_cases and top_resolving_articles, which the file did not count.
  • "narrows as the preset narrows" (cases_by_origin): 1 / 1 / 2 / 3 / 7 / 4 / 7 / 8 / 17 / 2 / 4 / 9 / 13 fixture rows over the 13 presets, the unwindowed 20 of 20, not inert, no nested pair answering more.
  • daily_case_volume is unbound; the file ran it with no window.

0 contradictions with what describe 2 asserted.

Verification

All at 3228b5954b, the merge of origin/main 0d43004100, after pnpm install --frozen-lockfile and turbo run build --filter=@objectstack/cli... (60 tasks), then --filter=!@objectstack/docs (74 tasks). Each run went through scripts/pm/os-verify-lock.sh, and each reads VERDICT command-exit 0.

  • pnpm --filter @objectstack/verify exec vitest run --maxWorkers=2: Test Files 37 passed (37), Tests 299 passed (299). dashboard-window.test.ts alone: Tests 10 passed (10).
  • pnpm --filter @objectstack/verify typecheck && pnpm --filter @objectstack/cli typecheck: check:test-typecheck: OK for both. With tsc --listFilesOnly -p tsconfig.test.json, dashboard-window is in verify's program (2 hits) and verify-dashboard-windows in cli's (1 hit).
  • pnpm --filter @objectstack/cli exec vitest run --project integration --maxWorkers=2 over the eight test/verify-*.test.ts files: Test Files 8 passed (8), Tests 24 passed (24). The rest of the integration tier is declared to CI.
  • pnpm --filter @objectstack/cli exec vitest run --project unit --maxWorkers=2: Test Files 285 passed (285), Tests 4283 passed (4283).
  • pnpm --filter @objectstack/core exec vitest run --project repo --maxWorkers=2: Test Files 6 passed (6), Tests 83 passed (83). The security(data, analytics): a lookup target's exposure declaration is not judged when the data door's $expand, or the dataset door's dimension-label pass, reads it — detail withheld pending maintainer #22661 pin second-object-read-exposure.pin.test.ts is among them, with no CALLERS row needed: the family never calls referenceTargetOf.
  • pnpm check:verify-stand-in exited 1 before the two NOT_A_STAND_IN rows (2 problem(s): runDashboardWindowProofs, formatDashboardWindowReport) and exits 0 after (2 stand-in check(s) guarded, 17 candidate(s) explicitly exempt, 10 call site(s) reached).
  • Gates: node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack with no paths derived 115 commands, the same 115 as over this PR's 9 paths. 115 run, all exit 0, and --ran reports 115 derived famil(ies) accounted for — 115 run, 0 NOT-MEASURED. Three of them (spec check:skill-examples, check:dual-build-cjs-loads, check:i18n-coverage) first answered exit 3, PREREQUISITE NOT MET, and were measured after the 74-task build.
  • Lint, as a proven narrowing: eslint --no-inline-config --format json over the 6 touched source files reads 6 files, 0 errors, 0 warnings, counted from the JSON. Population: --print-config resolves a config for all 6; the three Markdown files answer "File ignored because no matching configuration was supplied". Invariance: eslint.config.mjs never enables type-aware linting (its comment at about :327; no parserOptions.project), so this diff cannot move an untouched file's verdict.
  • hotcrm runs B and C re-taken at 3228b5954b: the same counts as the table above.

Acceptance notes

  • The shipped console closes the three rolling presets at {today} (objectui dashboard-filters PRESET_RANGES, at the .objectui-sha pin 7578323a), while the spec's DATE_RANGE_PRESET_MACRO_WINDOWS leaves them open. The fixtures carry no day after today and the differential cancels every other row, so the verdict is the same under either; the README and cli.mdx say so.
  • created_at as the built-in date range's default field lives as a literal in the lint, in objectui and now here; no spec export carries it.
  • The fixture helpers (fixtureBody, vary, laterBy, uniqueFields) are now copied in four family modules, each module-private, as slice 4's record noted; a shared one belongs in derive.ts, outside this claim.
  • The family measures count widgets only. A widget whose only measures are a sum, an average or a derived ratio is excluded and counted.

Deviations

  • The window handed to the door is the spec's DATE_RANGE_PRESET_MACRO_WINDOWS, not hotcrm's { $gte: '{N_days_ago}', $lte: '{today}' }. That pair is the console's own lowering (objectui PRESET_RANGES), a table in the sibling repo; copying it here would be a fourth copy of the vocabulary date-range-presets.ts was consolidated to end. The spec's table is the in-repo statement of each preset's window. The analytics door's own preset arm (timeDimensions[].dateRange, lowered by core's resolveAnalyticsDateRangePreset) was not used: it needs a dataset dimension, while a dashboard window binds an object field.
  • The principal is the dev admin. A plain member is refused 403 reading the object, so the family takes the caller's token (os verify passes signIn()'s), as the CRUD, reference-shape, delete-behaviour and unique-scope families do. Fixture rows are owned by it.
  • Fixtures are written per widget and count measure, and every count is a difference. The window field is the probe, and the app's own rows cancel out, so no measurement depends on the stack being empty. The unwindowed control leg is what makes a fixture that misses the widget's selection read inconclusive, never contradicted.
  • The negative controls are an app plugin's aggregate middleware (the engine's published seam), which bootStack and os serve compose identically, so the same control runs from the spawned CLI.
  • $nin / $ne are held only on a field that declares options (the first option not excluded). Without that, five of hotcrm's opportunity count widgets would be excluded.
  • A refused smallest record is retried once with every synthesized field. It is pinned by a fixture hook; on hotcrm, won_deals needs it.
  • hotcrm evidence comes from direct runs of the family, as slices 1-4 did (protocol ^17.7.0 against 18). The 76 email_template.category tombstones were removed in memory.
  • origin/main was merged once, at 0d43004100, and the build was refreshed; one conflict in cli.mdx, beside finding(security): every write to a controlled_by_parent detail TWO levels below its root master answers 403 "master … not editable" for a principal who edits that master — a regression against 17.7.0, measured on hotcrm's crm_quote_line_item #23093's new sentence, was resolved keeping both. main then moved 6 more commits, to d206d08cab, none on this PR's files. One, f931e765b7, refines DashboardHeaderActionSchema.actionUrl, which the family's dashboard parse reads; hotcrm's dashboards declare no header actions, so its counts cannot move. finding(verify): bootStack composes no MetadataPlugin, so a stack's declared apis answer 404 ENDPOINT_NOT_FOUND there while os serve of the same stack answers them — its "what serve composes" rule (#22301, ruling A) has a gap #23088 had not landed at either reading, so there is one composition, and both runs are on it.
  • Commits carry the model-free trailer pair, as AGENTS.md says, and this body ends with its session-URL footer.

Generated by Claude Code

deriveDashboardWindowCases / runDashboardWindowProofs /
formatDashboardWindowReport: every widget a dashboard's dateRange binds
counts, under each preset of DATE_RANGE_PRESETS, the fixture rows that
window selects, asked of POST /analytics/dataset/query as the signed-in
principal. The binding is read through DashboardSchema / DatasetSchema,
the window from DATE_RANGE_PRESET_MACRO_WINDOWS (handed as tokens), the
ground truth's day boundaries from core's resolveFilterTokens. Fixture
suite with two composed negative controls and an attribution control.

Claude-Session: https://claude.ai/code/session_01VoSxBQujKLZKPwK2u5ehQ6
Co-authored-by: Claude <noreply@anthropic.com>
…ow family (opt-in)

A fresh stack of its own, the admin principal, hardFailures counted, the
report under --json as dashboardWindows. The family's controls move to an
app plugin's aggregate middleware (the engine's published seam), which
os verify composes exactly as bootStack does; an omitted column on read
back is reported unconfirmed, not rewritten.

Claude-Session: https://claude.ai/code/session_01VoSxBQujKLZKPwK2u5ehQ6
Co-authored-by: Claude <noreply@anthropic.com>
…d-in rows; the docs (#15953 slice 5, WIP)

Claude-Session: https://claude.ai/code/session_01VoSxBQujKLZKPwK2u5ehQ6
Co-authored-by: Claude <noreply@anthropic.com>
… field, and the report names the shape (#15953 slice 5, WIP)

Claude-Session: https://claude.ai/code/session_01VoSxBQujKLZKPwK2u5ehQ6
Co-authored-by: Claude <noreply@anthropic.com>
… the dashboard date-window family (#15953 slice 5)

Claude-Session: https://claude.ai/code/session_01VoSxBQujKLZKPwK2u5ehQ6
Co-authored-by: Claude <noreply@anthropic.com>
# Conflicts:
#	content/docs/deployment/cli.mdx
@github-actions github-actions Bot added size/xl documentation Improvements or additions to documentation tests tooling labels Oct 13, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/cli, @objectstack/verify, touching 42 documentable anchor(s). ⚠️ 2 changed file(s) yielded no anchor (packages/verify/README.md, packages/verify/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.

30 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: node scripts/docs-audit/affected-docs.mjs --json d206d08cab87f4df16be918a231a3cc657e2bfe7.

⛔ 6 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails.

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

Coarse fallback — 30 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 d206d08cab87f4df16be918a231a3cc657e2bfe7 → packageMentionDocs.

Which tree this was computed on

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

node scripts/docs-audit/affected-docs.mjs --json d206d08cab87f4df16be918a231a3cc657e2bfe7

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

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 3228b5954be2baccd7319e70a3d0f637f1e641a0
Local-runs: none

Read at 2026-10-13T04:36Z, adversarially, against AGENTS.md Prime Directives #10 and #12 and Post-Task Checklist step 3. Inputs: card #15953 (body and all 26 comments: the classification table 6115219467 row 9, the slice 5 claim 6135014332, the dev report 6137873343, the seat's ACCEPT 6138184218 read as a claim, and slice 4's records 6131842119 / 6134145854 on PR #23091 as the precedent); PR #23150's body, file list and the net diff of refs/pull/23150/head against merge base 0d43004100 (9 files, +1820 / -9, 7 commits including one merge of origin/main); the check-runs on the head, read twice. Nothing was built, run or re-run; the only local commands were git fetch / git diff / git show / git log / git merge-base / git ls-tree against the fetched ref, the template print, gh api reads, and reads of untouched spec, core, analytics, REST, lint and harness files at the head for the seams the family names.

① Derived judgments

The three functions and twelve types, reachable and coherent: right. packages/verify/package.json maps . to dist/index.js / dist/index.d.ts (and the require pair); packages/verify/src/index.ts exports deriveDashboardWindowCases, runDashboardWindowProofs, formatDashboardWindowReport and exactly the twelve types the changeset lists from ./dashboard-window.js; the index diff is additive (the header count "Six" to "Seven" and the new block; nothing else moved). The shapes follow the siblings: deriveXCases(config) / runXProofs(stack, token, config, options) / formatXReport(report), the token taken as the CRUD, reference-shape, delete-behaviour and unique-scope families take it; app is the same expression; summary.hardFailures is contradicted + failed widgets, and README, cli.mdx and the changeset each say so. The gate only ever reads what the run measured: proven needs every probed measure to hold under every preset, after the unwindowed control leg counted every fixture row; inconclusive, unbound and excluded are reported and not counted. Promises no more than measured, with one docblock hazard on a public type: DashboardWindowProofOptions.now says "The reference instant the ground truth resolves on" — literally true, but the server resolves the same tokens on its own clock (resolveSelectionTokens, new Date() per request), so a now that is not the current instant makes every window read contradicted. Nothing in the suite passes it. Owed one docblock sentence (pass the current instant or nothing) or the option's removal, on the next push to this file. Not a FAIL: it defaults right and no document advertises pinning the run.

os verify --dashboard-windows: right. Flags.boolean with default: false, typed into runVerification. With the flag, bootStack(config, { multiTenant, hostRoot }) is a fresh stack stopped in finally, signIn() is the principal, dashboardWindows.summary.hardFailures is added under ?? 0, the report rides emitJson (a JSON.stringify, so an undefined value emits no key), and the text branch is if (dashboardWindows). Without the flag nothing moves but help text (the description, one examples row, one flag), a widening the minor carries. packages/cli/test/verify-dashboard-windows.test.ts spawns the real process and pins it: no dashboardWindows key and exit 0 without the flag; proven, 13 probes, 13 held, exit 0 with it; contradicted, inert, hardFailures 1, exit 1 on the same app plus its own window-dropping plugin, composed through defineStackSourceFromLiteral, so the spawned path carries the same control the library suite does.

Where each expectation comes from: right, with two restated consumer rules named and immaterial to the door-scoped claim.

  • The binding. Every dashboard is DashboardSchema.safeParse, every dataset DatasetSchema.safeParse; the field is filterBindings.dateRange (string = that field, false = unbound, absent = dateRange.field || 'created_at'), resolved against dataset.object as the booted registry holds it (stack.metadata.object, system columns injected, so created_at resolves) and refused unless date / datetime. The spec's dateRange.field is z.string().optional(), and the 'created_at' default lives in DashboardWidgetSchema.filterBindings' docblock and nowhere else: the lint's own copy (validate-widget-bindings.ts:604) says it "MUST track objectui dashboard-filters.ts", i.e. the default is applied by the console, not by any server. The family therefore applies a CONSUMER rule, sourced to the spec's docblock, which is the only in-repo statement of it; a third copy (lint, objectui, verify). The dev names it and the remedy (a spec export) is outside this claim. An empty-string field falls to the default, the lint's reading too.
  • What a preset hands the door. DATE_RANGE_PRESET_MACRO_WINDOWS for each of the 13 DATE_RANGE_PRESETS, handed as the token pair { $gte: start, $lte: end } ($lte absent for the three rolling presets), never lowered. The server's resolveSelectionTokens (dataset-executor.ts:118) resolves selection.runtimeFilter with filterTokenContextFrom(context, new Date()) into core's resolveFilterTokens, the same function the family calls with { now: start, timezone, userId } for the ground truth; {today} and its kin spell YYYY-MM-DD (asYmd), and {current_user_id} resolves to context.userId, the principal the family holds. Read from source, not re-derived.
  • The day boundary. The ground truth compares a fixture's day with the resolved pair by the table's own convention (first and last day inclusive, the rolling end open); the engine compiles a bare-day $lte on a datetime column as strictly-before nextUtcCalendarDay(day) (calendar-day.ts, ADR-0053 D-D), which on a date column is order-equivalent to at-or-before the day, so a noon stamp on the last day is in both. The convention is restated as a string comparison rather than read from an engine helper; right for a ground truth, which must not be the engine checking itself, and the convention is the table's published text.
  • The $and composition. mergeFilters appears in this repo only as objectui facts (packages/spec/liveness/report.json, the 2026-06 report-liveness audit): a restated consumer rule. What this repo publishes is the door's own composition, combineFilters(compiled.filter, selection.runtimeFilter) → { $and: [a, b] } (dataset-executor.ts:151-157, applied at :1113), with measure filters applied per measure pass. It does not matter for the claim: the property measured is the door's count under a conjunction of the widget's filter and the window, every conjunction spelling selects the same rows, and the console's spelling is a sibling-repo fact the family cannot measure. README, cli.mdx and the changeset scope the promise to POST /analytics/dataset/query, so the claim is as narrow as the measurement.

The door, and the attribution: sound. POST {basePath}/analytics/dataset/query resolves body.datasetName through p.getMetaItems({ type: 'dataset' }) and answers 404 on an unknown name (rest-server.ts), parses the selection through the door parse, and runs queryDataset under the request's ExecutionContext — the request a dashboard widget makes, as the signed-in principal. A plain member refused 403 reading the object is the dev's measurement, consistent with #16645's object-level read grant on this door; taking the caller's token is the sibling families' construction. Every count is after − before under the same window, so the app's own rows cancel; the control leg (unwindowed.answered === written.length) gates every window and also catches a concurrent writer or a misread measure key; the stamp and every held column are read back through the system read and compared with coercion tolerance (sameValue), a rewritten or omitted column making the probe not-run; a non-UTC principal timezone, a refused unwindowed query, a refused fixture and a required relation that cannot be seeded are each not-run; a day turning between day0 and the after-queries is not-run. Under those gates a count that differs from the truth is attributable to the window. The two negative controls go through the engine's published seam (registerMiddleware with { object }, op.operation === 'aggregate', op.ast.where), no engine source changed, and the ablations of judge red exactly their controls as reported. ONE GAP, red-direction: the presets are resolved once at start (options.now ?? new Date()), but day0 is read per measure and the fixture days are placed on day0; a run that crosses UTC midnight between start and a measure's probe judges that measure against yesterday's resolved edges while the server resolves today's, and reads contradicted (for example last_7_days: expected the days at or after the stale start, answered one fewer). The guard compares todayAt(new Date()) with day0, never with todayAt(start). A false RED in a rare window, never a false green; the documented class ("the day turned mid-probe") names it but the guard does not fully cover it. Owed a one-line guard (day0 !== todayAt(start) → not-run) on the next push to this file. Not a FAIL.

The exclusions: excluded, not modelled. holdColumns returns a reason for every $-operator key ($or among them), a relationship path, a condition on the window field itself, a field the object does not declare, any condition other than a bare scalar, $eq, a non-empty $in (first member), or $nin / scalar $ne on a field that declares options (first option not excluded; none left is a reason too), any placeholder other than {current_user_id}, a non-scalar value, and two holds that disagree. Measures: a derived measure, a non-count aggregate, a count of a relationship path. Widgets: an unparsed dashboard, an undeclared or unparsed dataset, an undeclared object (sys_user), an object the CRUD derivation cannot synthesize, a missing or non-temporal field judged on the booted registry. unbound is filterBindings.dateRange === false only. The derivation test and the booted test pin every kind with a count table (19 widgets: 10 proven, 1 unbound, 8 excluded by five kinds; 1 measure excluded beside a probed one).

hotcrm: honestly scoped, read as reported. No run was made here. The PR body states that the family measures count widgets only, that describe 2 also blank-checked three non-count widgets (avg_resolution_time, sla_compliance_gauge, kb_deflection_rate) which the family excludes and counts, that daily_case_volume is unbound where the file ran it windowless, and that the evidence came from direct runs over slice 3's artifact (protocol ^17.7.0 against this tree's 18, the 76 tombstones removed in memory), as slices 1-4 did. "0 contradictions with what describe 2 asserted" is true of what the family measured — the seven case_count widgets the file compared, plus three it did not, under all 13 presets, run C — and for the three non-count blank checks the family reports excluded, which is not the file's finding; the dev and the seat both name that residue. The 3 inconclusives of runs A and B (is_closed derived by hotcrm's own hook) are the attribution control on a real app. See ③ for the two standing notes.

Docs sentences against the diff: right. Changeset: three functions, twelve types (counted against index.ts), the signature runDashboardWindowProofs(stack, token, config, { now? }), the three sources, the fixture rule (smallest record, then every synthesized field — fixtureBody(full) and the one retry), the six statuses with each inconclusive arm matching a notRun site, the excludedKind list, the held-column rule, hardFailures, and the CLI paragraph ("without the flag the run and its document are unchanged") all match dashboard-window.ts and verify.ts. README: "Seven app-agnostic proof families" (index.ts says seven), the bullet, the opt-in paragraph, the exit-code sentence (contradicted or failed), the status table (six rows matching widgetStatus plus unbound and excluded), the API line. cli.mdx: the example row, the paragraph in the os verify section only, and the --json key list with dashboardWindows ("its presets name what each preset handed the door and how it resolved" — condition and resolved, true). The merge 3228b5954b carried only main's own additions on cli.mdx and the README (#23093's sentence, present verbatim at the head) and the PR's own lines are byte-identical before and after it.

The NOT_A_STAND_IN rows: true of the code — and there are TWO, on three lines. runDashboardWindowProofs(stack: VerifyStack, ...) and formatDashboardWindowReport(report: DashboardWindowReport) are exactly the gate's candidates (parameter 0 annotated with a package type re-exported as a type), VerifyStack being the concrete handle bootStack returns and DashboardWindowReport a value this package produced; deriveDashboardWindowCases(config: any) is not a candidate. The rows' words are the sibling rows' words; the dev's "2 problem(s)" before the rows agrees; Lint & Repo Gates, which carries check:verify-stand-in, is green on the head. The ACCEPT's "3 NOT_A_STAND_IN rows" counts lines, not rows; harmless.

② Semver level

.changeset/15953-verify-dashboard-window-family.md: @objectstack/verify minor, @objectstack/cli minor. The diff publishes three new functions and twelve new types from @objectstack/verify and one new opt-in flag plus help text from @objectstack/cli; no existing export, signature, default or behaviour moves (the index.ts and verify.ts diffs are additive; scripts/check-verify-stand-in-erasure.mjs gains two ledger rows). Minor is the right level for both and meets the card's own acceptance ("@objectstack/verify minor per new exported family"). Not breaking, so no migration text and no ADR-0087 disposition marker is owed; the dev's check-adr-0087-registration reading agrees, and Check Changeset is success on the head (both runs, the last after the seat's label write).

Clause-② carriers: PR body line 2 Clause-②: yes; the changeset body Clause-②: yes; the claim 6135014332 Clause-②: yes. The three agree. Each is legal: yes with no arm is a complete declaration, and yes takes at least minor, which both entries are.

③ Boundary flags

Dev report 6137873343: open_questions empty. Its deviations and findings, each answered or escalated:

  • What a preset hands (assumption 3): answered in ① — the spec's table is the in-repo statement of each preset's window; hotcrm's { $gte, $lte: '{today}' } pair is the console's lowering, a sibling-repo table; over fixtures that never post-date today, with the differential, both select the same rows, so the verdict is the same under either. The divergence for future-dated rows is the class (b) finding below.
  • The door as the dev admin (assumption 4): answered in ① — a member is refused reading the object; the caller's token is the sibling families' construction; the saved dataset resolves by name today, so nothing waits on finding(verify): bootStack composes no MetadataPlugin, so a stack's declared apis answer 404 ENDPOINT_NOT_FOUND there while os serve of the same stack answers them — its "what serve composes" rule (#22301, ruling A) has a gap #23088 (not landed at either reading; one composition measured).
  • The ground truth as a difference, the control leg, $nin / $ne on declared options, the one retry with every synthesized field (assumption 5): answered in ① — each is a gate that keeps a non-window cause out of proven, and the retry is pinned by a fixture hook.
  • The negative controls through an app plugin's aggregate middleware (assumption 8): answered in ① — the engine's published seam, composed identically by bootStack and the spawned CLI; no engine source changed.
  • Date fields included (assumption 2): answered — due_on on the fixture app, close_date on hotcrm, the stamp taking the day itself, order-equivalent under the calendar-day rule.
  • Scratch files in slice 1's folder; two runs outliving the lock wrapper; the hotcrm clone left in place; commit trailers: process notes. The head tree carries no scratch, pid or log file (git ls-tree), so Post-Task Checklist step 7 holds for the repo; the reported counts come from runs completed under the lock. Not contract matters.
  • One merge of origin/main; main moved six more commits: answered in ① — the merge carried only main's own change; f931e765b7 refines DashboardHeaderActionSchema.actionUrl, which the family's parse reads, and hotcrm's dashboards declare no header actions, so its counts cannot move; the net diff against the merge base is the nine files reviewed.
  • Out-of-scope finding, class (b), reach at a public door — the rolling presets' three extents. The evidence is sound as stated. Read in source at the head: DATE_RANGE_PRESET_MACRO_WINDOWS hands last_7/30/90_days as $gte-only, and its docblock says in one breath that "the resolver's clock is the bound" and, for calendar presets, that $gte alone "selects every day AFTER the window on a column that carries future dates" — the second sentence is true of the rolling form too, since a $gte-only filter carries no clock at all; bareDateRangePresetComparandMessage prescribes that open form to authors at the save door and the lint; core's resolveAnalyticsDateRangePreset closes the same three presets at now (inclusive, analytics-date-range.ts:189-193); the console's PRESET_RANGES closing at {today} is the dev's read of objectui at the pin (sibling repo, not verified here). Three extents for one preset name, and the measured 2 / 1 / 1 on today−5 and today+5 is exactly what they predict. Under Prime Directive chore: version packages #10 that is a contract violation with a repro and an authoring trap (the prescription an author is handed selects a different extent than the picker it is prescribed for). It does not move the head: the family's fixtures never post-date today and the differential cancels an app's own future rows. ESCALATED: the seat takes it to the filing gate in this round, as the ACCEPT says; lane (spec's table and message, core's arm) and grade are triage's.
  • Finding, carrier none — the created_at literal: a note is right; now three copies (lint, objectui, verify), each sourced to the same docblock; a spec export is the remedy, outside this claim. Not a defect under chore: version packages #10.
  • Finding, carrier none — fixtureBody / vary / laterBy / uniqueFields in four family modules: slice 4's finding 5, one copy later; a refactor into derive.ts, outside this claim. A note is right.
  • Finding, carrier none — count widgets only. A note is right for the family, and a STANDING NOTE for the card: row 9's D residue is "describe 2", and describe 2's blank check over three non-count widgets is not measured by this family (an average or ratio has no row-level ground truth without modelling the measured column). The card's acceptance clause "reports the same findings the retired hotcrm file asserted" is met for every count widget and not for those three. ESCALATED to the card: hotcrm's retirement of describe 2 keeps those three blank checks until a later slice measures them, or the card re-scopes them.
  • Finding, carrier none — hotcrm's is_closed hook (runs A and B): the attribution control doing its job; slice 4's finding 4 class. No carrier.
  • Finding, carrier none — an internal: true window field: the system read omits it and the door refuses it as a filter key, so such a binding reads inconclusive here and can only ever refuse at the door. A note is right; not this PR's.
  • The hotcrm clause ("runs from os verify on hotcrm's metadata"): this tree is protocol 18 and hotcrm pins ^17.7.0, so the clause cannot be met on this tree for this slice either; the same open acceptance note slices 1-4 landed under. ESCALATED, not a FAIL: the card stays open for its remaining rows, and whoever closes it owes that clause a run or a re-scope.
  • Added by this review: the midnight gap between start and day0 (①), owed a one-line guard on the next push to dashboard-window.ts; the DashboardWindowProofOptions.now docblock (①), owed a sentence; the ACCEPT's row count (①), harmless. None is a carrier.

Check-runs on the head, read three times in this act, the last at 2026-10-13T04:36Z, latest run per name. 41 names; at the final read every run had concluded: 34 success, 7 skipped, 0 failure, 0 in progress (the ACCEPT's "36 success and 5 skipped" counts the two labeled re-runs' earlier successes as latest; same runs, different dedup). Success: Build Core, Build Docs, Check Changeset (the 04:23Z run after the seat's label write, success), Check Documentation Links, Dogfood Regression Gate, Flag docs affected by code changes, Governed Surface Queue Guard (no governed path among the nine files; scripts/check-verify-stand-in-erasure.mjs is not under .claude/**), Lint & Repo Gates, the eight Lint · jobs, the four PR-shape guards, Spec property liveness, Test Core and its six shards, the four Type Check · jobs, TypeScript Type Check and filter. Skipped: Auto Label and Check PR Size on their 04:23Z labeled re-run (their 03:52Z runs on this same head were success), and the five roster skips on a PR run (Console Pin Gate, the Dogfood Regression Gate matrix row, Dogfood Verify CLI, Packed-tarball smoke (opt-in), Temporal Conformance (live PG + MySQL)), as slice 4's record found. No gate was re-run by this act. What the seat owes before the queue, none of it in this diff: the class (b) carrier, the two standing card notes, and the sequence the ACCEPT set (after #23148 clears); what the next push to dashboard-window.ts owes: the midnight guard and the now docblock sentence.

Implemented-by: claude/issue-15953-s5-dashboard-window
Reviewed-by: session_01VoSxBQujKLZKPwK2u5ehQ6

VERDICT: PASS


Generated by Claude Code

This branch has not been deployed

No deployments
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.

2 participants