Skip to content
38 changes: 38 additions & 0 deletions .changeset/15953-verify-dashboard-window-family.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
---
'@objectstack/verify': minor
'@objectstack/cli': minor
---

feat(verify): the dashboard date-window family — every widget a dashboard's `dateRange` binds counts, under each preset of the picker, the rows that window selects, runnable as `os verify --dashboard-windows` (#15953)

Clause-②: yes

A seventh derived proof family beside `runCrudVerification`, `runRlsProofs`, `runReferenceShapeProofs`, `runDeleteBehaviorProofs`, `runUniqueScopeProofs` and `runParentWriteReachProofs`. It needs no hand-written test: from the app's own dashboards and datasets, read through the spec's own parse (`DashboardSchema`, `DatasetSchema`), it takes every dashboard that declares a `dateRange` and every widget that window binds, and asks the dataset door what each one counts.

Where each half of the promise is read from:

- the field a widget's window lands on is `DashboardWidgetSchema.filterBindings`' contract: `filterBindings.dateRange` names it, `false` opts the widget out (reported `unbound`), and absent means the dashboard's `dateRange.field` (`created_at` when unauthored). It resolves against the object behind the widget's dataset, as the stack registers it, and must be a `date` or `datetime` field;
- a preset's window is `DATE_RANGE_PRESET_MACRO_WINDOWS`, the `{date-macro}` pair each of the 13 `DATE_RANGE_PRESETS` names. It is handed to `POST /analytics/dataset/query` as written (`{ $gte: '{7_days_ago}' }`, `{ $gte: '{month_start}', $lte: '{month_end}' }`), ANDed with the widget's own filter as `runtimeFilter`, and the server resolves the tokens;
- the days a window selects, for the ground truth, are that same window resolved by `@objectstack/core`'s `resolveFilterTokens` on the caller's reference timezone (UTC unless the `ExecutionContext` carries one), first and last day inclusive.

Per bound widget and count measure, fixture rows are written through the seed door on every preset's first and last day and the day either side (never after today), each stamped at UTC noon (a `date` field takes the day itself), owned by the caller, with every column the widget's filter, the dataset's filter and the measure's filter compare held to a value they select, and read back before anything is judged. The widget is asked as the signed-in caller with no window and under every preset, before and after the fixtures, so each count is a difference and the app's own rows cancel out. With no window it must count every fixture row before any window is judged. Under each preset it must count exactly the fixture rows that window selects: a widget that shows nothing there is `blank`, one that counts another number is contradicted, and one whose every preset answers what no window answers is named inert.

A widget is `proven`, `contradicted` (a preset went blank or counted other rows), `failed` (the door refused or faulted on a windowed query while the widget's own query answered), `inconclusive` (a fixture could not be written or did not read back as written, the widget with no window did not count every fixture row, its own query is refused, the day turned mid-probe, or the caller's timezone is not UTC), `unbound` or `excluded` (`excludedKind` names why: no count measure, a filter the ground truth cannot model, a window on a missing or non-temporal field, an undeclared or unparsed dataset or dashboard, an object the app does not declare or the CRUD derivation cannot synthesize). `contradicted + failed` widgets are the report's `hardFailures`. An average, a sum or a derived measure is counted as excluded; a held column is an equality, an `$in`, a `$nin` or `$ne` on a field that declares its options, or `{current_user_id}`.

**`@objectstack/verify` — new exports.**

- `runDashboardWindowProofs(stack, token, config, { now? })` → `DashboardWindowReport`. Run it on a fresh stack of its own, as a principal that reads the dashboards' objects: it writes fixture rows.
- `deriveDashboardWindowCases(config)`, `formatDashboardWindowReport(report)`.
- Types: `DashboardWindowExclusion`, `DashboardWindowMeasureCase`, `DashboardWindowCase`, `DashboardWindowDashboard`, `DashboardWindowPreset`, `DashboardWindowAnswer`, `DashboardWindowPresetProbe`, `DashboardWindowMeasureResult`, `DashboardWindowWidgetStatus`, `DashboardWindowWidgetResult`, `DashboardWindowReport`, `DashboardWindowProofOptions`.

```ts
import { bootStack, runDashboardWindowProofs, formatDashboardWindowReport } from '@objectstack/verify';

const stack = await bootStack(myApp);
const report = await runDashboardWindowProofs(stack, await stack.signIn(), myApp);
console.log(formatDashboardWindowReport(report)); // report.presets names what each preset handed the door
expect(report.summary.hardFailures).toBe(0);
await stack.stop();
```

**`@objectstack/cli` — `os verify --dashboard-windows`.** Opt-in, like `--unique-scopes` and `--parent-write-reach`, on a fresh boot of its own, as the signed-in admin. A widget that went blank or counted other rows than its window selects, or a windowed query the door refused or faulted, counts in `hardFailures` and fails the run. The `--json` document carries the report as `dashboardWindows` when the flag is given; without the flag the run and its document are unchanged.
38 changes: 36 additions & 2 deletions content/docs/deployment/cli.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -1926,6 +1926,7 @@ os verify --reference-shapes # Also prove reference fields ref
os verify --delete-behaviors # Also prove each declared deleteBehavior is what a delete does
os verify --unique-scopes --multi-tenant # Also prove each declared unique collides where its scope says
os verify --parent-write-reach # Also prove each controlled_by_parent detail write follows its master
os verify --dashboard-windows # Also prove each dashboard date picker counts the rows its window selects
```

1. **Author-time rules.** The rule registry `os validate` runs, over the stack
Expand Down Expand Up @@ -2016,6 +2017,37 @@ os verify --parent-write-reach # Also prove each controlled_by_p
admitted exactly when the caller holds the verb on the detail and may update
the middle master directly, which also needs edit access to the root, and
refused otherwise, so such a chain is reported proven.
With `--dashboard-windows`, another fresh boot proves that every dashboard
declaring a `dateRange` answers under each preset of its picker with the
rows that window selects. A widget's window lands on the field its
`filterBindings.dateRange` names, or on the dashboard's `dateRange.field`
(`created_at` when unauthored), resolved against the object behind the
widget's dataset, and must be a `date` or `datetime` field; a widget with
`filterBindings: { dateRange: false }` is reported `unbound`. For each count
measure of a bound widget, fixture rows are written through the seed door
on every preset's first and last day and the day either side (never after
today), stamped at UTC noon, owned by the caller and holding every column
the widget's own filter, the dataset's filter and the measure's filter
compare (the smallest record first, every synthesized field if the seed
door refuses it), and read back. The widget is then asked through
`POST /analytics/dataset/query` as the signed-in admin with no window and
under each of the 13 presets, before and after the fixtures. A preset is
handed as the spec's `{date-macro}` window for it (`{ $gte: '{7_days_ago}' }`
for `last_7_days`, `{ $gte: '{month_start}', $lte: '{month_end}' }` for
`this_month`), ANDed with the widget's filter, and the server resolves the
tokens; the ground truth resolves the same window with the platform's
resolver, on UTC calendar days unless the caller's context names a
timezone. Each difference must equal the fixture rows the window selects: a
widget that shows nothing there, or counts another number of them, fails
the run, and one whose every preset answers what no window answers is named
inert. The widget with no window must count every fixture row first; a
probe whose fixtures do not meet the widget's own selection, or that cannot
be written or read back as written, is reported inconclusive, never proven.
A held column is an equality, an `$in`, a `$nin` or `$ne` on a field that
declares its options, or `{current_user_id}`; an average, a sum or a derived
measure, and a filter the ground truth cannot model (`$or`, a range, a
relationship path, a condition on the window's own field), are reported
`excluded`.
`--multi-tenant` boots org-scoped so tenant-isolation RLS policies
apply; a walled tenancy posture (`OS_TENANCY_POSTURE` set to `isolated` or
`group`) asks for the same boot.
Expand Down Expand Up @@ -2047,8 +2079,10 @@ document carries depends on where the run ended:
`deleteBehaviors` (with `--delete-behaviors`), `uniqueScopes` (with
`--unique-scopes`; its `boot` names the legs the run could prove),
`parentWriteReach` (with `--parent-write-reach`; its `personas` name who
wrote and who could not be provisioned) and `hardFailures`, the count
the exit status is read from.
wrote and who could not be provisioned), `dashboardWindows` (with
`--dashboard-windows`; its `presets` name what each preset handed the door
and how it resolved) and `hardFailures`, the count the exit status is read
from.
- **Could not run:** `{ "error": "<sentence>" }`, plus `code` and `httpStatus`
when the failure carries them.

Expand Down
38 changes: 35 additions & 3 deletions packages/cli/src/commands/verify.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,15 @@ import {
runParentWriteReachProofs,
formatParentWriteReachReport,
parentWriteReachSecurity,
runDashboardWindowProofs,
formatDashboardWindowReport,
type VerifyReport,
type RlsReport,
type ReferenceShapeReport,
type DeleteBehaviorReport,
type UniqueScopeReport,
type ParentWriteReachReport,
type DashboardWindowReport,
type RlsProbeDescriptor,
type RlsPositionPersonaInput,
} from '@objectstack/verify';
Expand Down Expand Up @@ -108,12 +111,16 @@ export function resolveVerifyMultiTenant(flags: { 'multi-tenant'?: boolean }): b
* or create of a detail, written through the engine as the persona,
* is admitted exactly when it holds that verb on the detail's object
* and can edit the master
* - dashboard date windows (--dashboard-windows): every widget a
* dashboard's `dateRange` binds counts, under each preset of the
* picker, the fixture rows that window selects, asked of the dataset
* door as the signed-in principal
*
* Exits non-zero on either stage's failures so it drops straight into CI.
*/
export default class Verify extends Command {
static override description =
'Run the author-time rules os validate runs, then boot the app in-process and verify it through the real HTTP stack (CRUD round-trip fidelity, plus the cross-owner RLS invariant, the reference-value shape proofs, the declared delete-behaviour proofs, the declared unique-scope proofs and the parent-derived write reach proofs on request)';
'Run the author-time rules os validate runs, then boot the app in-process and verify it through the real HTTP stack (CRUD round-trip fidelity, plus the cross-owner RLS invariant, the reference-value shape proofs, the declared delete-behaviour proofs, the declared unique-scope proofs, the parent-derived write reach proofs and the dashboard date-window proofs on request)';

static override examples = [
'<%= config.bin %> verify',
Expand All @@ -123,6 +130,7 @@ export default class Verify extends Command {
'<%= config.bin %> verify --delete-behaviors',
'<%= config.bin %> verify --unique-scopes --multi-tenant',
'<%= config.bin %> verify --parent-write-reach',
'<%= config.bin %> verify --dashboard-windows',
];

static override flags = {
Expand All @@ -146,6 +154,10 @@ export default class Verify extends Command {
description: "Also prove every field declared unique collides as its scope says (true / 'organization': within one organization only; 'global': across the installation), through the REST door and the system door; the cross-organization half needs --multi-tenant and is reported not run without it (ADR-0120 unique scopes)",
default: false,
}),
'dashboard-windows': Flags.boolean({
description: "Also prove, for every dashboard that declares a dateRange, that each widget it binds counts, under each date-range preset of the picker, the rows that window selects: fixture rows written on known days, the widget asked through the dataset door with and without the window (date and datetime fields; count measures)",
default: false,
}),
'parent-write-reach': Flags.boolean({
description: "Also prove, for every controlled_by_parent object, that a persona's edit, delete or create of a detail is admitted exactly when it holds that verb on the detail's object and can edit the master, written through the engine as verifier personas and one persona per declared position (ADR-0055 master-detail write reach)",
default: false,
Expand Down Expand Up @@ -231,6 +243,7 @@ export default class Verify extends Command {
'delete-behaviors': boolean;
'unique-scopes': boolean;
'parent-write-reach': boolean;
'dashboard-windows': boolean;
'multi-tenant': boolean;
json: boolean;
}): Promise<void> {
Expand Down Expand Up @@ -387,6 +400,20 @@ export default class Verify extends Command {
}
}

// The dashboard date-window proofs run on a fresh stack of their own too:
// they write fixture rows on known days, which another phase should not
// count, and which no other phase's rows should land among.
let dashboardWindows: DashboardWindowReport | undefined;
if (flags['dashboard-windows']) {
const windowStack = await bootStack(config, { multiTenant, hostRoot });
try {
const adminToken = await windowStack.signIn();
dashboardWindows = await runDashboardWindowProofs(windowStack, adminToken, config);
} finally {
await windowStack.stop();
}
}

// Failure contract: a "real" runtime break the app's author must see.
// A degraded RLS probe counts: the run reported verdicts it could not have
// established, which is worse than no verifier at all.
Expand Down Expand Up @@ -418,20 +445,25 @@ export default class Verify extends Command {
(uniqueScopes?.summary.hardFailures ?? 0) +
// A detail write whose answer disagreed with the conjunction, a door
// that faulted on one, or a persona that could not be provisioned.
(parentWriteReach?.summary.hardFailures ?? 0);
(parentWriteReach?.summary.hardFailures ?? 0) +
// A bound widget that went blank or counted other rows than its window
// selects, or a windowed query the door refused or faulted. Inconclusive,
// unbound and excluded widgets are reported, not counted.
(dashboardWindows?.summary.hardFailures ?? 0);

if (flags.json) {
// `emitJson`, not `this.log`: stdout is reserved (see `run()`), so the
// report has to leave through the real stream — and awaiting the write
// drains a report larger than one pipe buffer before `this.exit` below.
await emitJson({ app: crud.app, config: absolutePath, multiTenant, crud, rls, referenceShapes, deleteBehaviors, uniqueScopes, parentWriteReach, hardFailures });
await emitJson({ app: crud.app, config: absolutePath, multiTenant, crud, rls, referenceShapes, deleteBehaviors, uniqueScopes, parentWriteReach, dashboardWindows, hardFailures });
} else {
this.log(formatReport(crud));
if (rls) this.log(formatRlsReport(rls));
if (referenceShapes) this.log(formatReferenceShapeReport(referenceShapes));
if (deleteBehaviors) this.log(formatDeleteBehaviorReport(deleteBehaviors));
if (uniqueScopes) this.log(formatUniqueScopeReport(uniqueScopes));
if (parentWriteReach) this.log(formatParentWriteReachReport(parentWriteReach));
if (dashboardWindows) this.log(formatDashboardWindowReport(dashboardWindows));
this.log('');
this.log(
hardFailures > 0
Expand Down
Loading
Loading