Repository navigation
feat(verify): the dashboard date-window family, runnable as os verify --dashboard-windows (#15953 slice 5) - #23150
objectstack-fleet[bot] wants to merge 7 commits into
Conversation
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>
…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
📓 Docs Drift CheckThis PR changes 2 package(s): 30 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: ⛔ 6 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails. What this run could not see
Coarse fallback — 30 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # 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
|
Contract reviewServed-tier: 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 ① Derived judgmentsThe three functions and twelve types, reachable and coherent: right.
Where each expectation comes from: right, with two restated consumer rules named and immaterial to the door-scoped claim.
The door, and the attribution: sound. The exclusions: excluded, not modelled. 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 ( Docs sentences against the diff: right. Changeset: three functions, twelve types (counted against The ② Semver level
③ Boundary flagsDev report
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 Implemented-by: VERDICT: PASS Generated by Claude Code |
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'stest/dashboard-date-range-window.test.tsdescribe 2 pins that by hand for its Service dashboard. It is now a derived proof family in@objectstack/verify(the seventh, besiderunCrudVerification), runnable as the opt-inos 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
dateRangeanswers, 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):
DashboardWidgetSchema.filterBindings(packages/spec/src/ui/dashboard.zod.ts). A string binds the window to that field,falseopts the widget out (unbound), and absent means the filter's own field, "dateRange:dateRange.field ?? 'created_at'". Dashboards and datasets are read throughDashboardSchema.safeParseandDatasetSchema.safeParse. The field resolves againstdataset.objectas the booted stack registers it, socreated_at(a registry-injected column) resolves. It must bedateordatetime. Thecreated_atliteral has no export; it is named once with its source, beside the lint's own copy (DATE_RANGE_DEFAULT_FIELDinvalidate-widget-bindings.ts).DATE_RANGE_PRESET_MACRO_WINDOWS(@objectstack/spec/data), for each of the 13DATE_RANGE_PRESETS. It is handed as written,{ $gte: start, $lte: end }(endabsent for the three rolling presets), ANDed with the widget's own filter as{ $and: [filter, window] }, the shipped console's merge (objectuimergeFilters). The server resolves the tokens (dataset-executor.tsresolveSelectionTokens), and the verifier lowers nothing.@objectstack/core'sresolveFilterTokens(the resolver the executor calls) on the caller'sExecutionContext.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/querywithdatasetNameand the widget's own selection (itsdimensions, one count measure, its filter with the window ANDed in asruntimeFilter), as the signed-in principal the caller passes in.os verifypasses the dev admin fromsignIn(). Measured onbootStackat21c86444ec:getMetaItems({ type: 'dataset' })(200), so nothing here waits on finding(verify): bootStack composes no MetadataPlugin, so a stack's declaredapisanswer 404 ENDPOINT_NOT_FOUND there whileos serveof the same stack answers them — its "what serve composes" rule (#22301, ruling A) has a gap #23088;403 PERMISSION_DENIEDreading the object, so the admin is the principal;timezone: 'UTC'.hotcrm's file called
AnalyticsService.queryDatasetin 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.
datefield takes the day itself.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.$in(its first member),$ninor$neon a field that declares options (the first option not excluded, so no row is NULL there),{current_user_id}, and$andof these. Anything else is an exclusion with its reason, never a guess, as hotcrm'struthMatchesthrows.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(withexcludedKind).hardFailures=contradicted + failedwidgets.Controls and ablations
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,hardFailures1. No engine source was changed.blank.inconclusive, never contradicted. Aninternal: truewindow field reads back "no audited_at (the read does not return that field)", unconfirmed.packages/cli/test/verify-dashboard-windows.test.tsspawnsos verify. Without the flag there is nodashboardWindowskey 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,hardFailures1, exit 1.judge):scripts/ablation-replace.mjs, inside a scratch script withtrap restore EXIT INT TERMon absolute paths, at3228b5954b. No build is involved: the test imports the module by a relative path.if (answered === expected) return { outcome: 'held', ... }becomes an unconditionalheldplusABLATION_15953_S5_A. Anchor 1 → 0, marker 0 → 1, blob36251c04→ff99b96d. RED:Tests 2 failed | 8 passed (10), the drop control and its summary line, as predicted. Restored: blob36251c04equals HEAD,git diff HEADempty.blankbranch becomesif (false)plusABLATION_15953_S5_B. Anchor 1 → 0, blob36251c04→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:
mainat0f84c9c7. The test file, every dashboard and every dataset are unchanged since1d7148bf;case.object.tschanged a comment only, andaccount.object.tslost one selectdefaultValuethat no windowed widget reads. The runs use slice 3's artifact at1d7148bf(built by hotcrm's own CLI 17.7.0) and itsos migrate meta --from 17snapshot.engines.protocolwas moved to^18and the 76email_template.categorytombstones were removed in memory, as slices 3-4 did. Nothing was written to hotcrm.os verifystill cannot boot hotcrm (protocol^17.7.0against 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 overcrm_case.created_at(datetime) andcrm_opportunity.close_date(date); 5 non-count measures beside them are excluded. That makes 195 windows per run.@objectstack/verify17.7.0bootStack, the artifact as builtIn every run: 0 blank, 0 inert, 0 narrowing violations. The 3 inconclusives in A and B are
kb_resolved_cases,closed_cases_totalandtop_resolving_articles: hotcrm's own hook derivesis_closedfromstatus, so a heldis_closed: truereads backfalse. The attribution control is doing its job on a real app.won_dealsmeets 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):service_dashboardis windowed oncreated_at, and so are 3 more dashboards the file never covered (onclose_date, adatefield).avg_resolution_time,sla_compliance_gauge,kb_deflection_rate); the family excludes those and counts them.case_countwidget 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 doclosed_cases_total,kb_resolved_casesandtop_resolving_articles, which the file did not count.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_volumeisunbound; the file ran it with no window.0 contradictions with what describe 2 asserted.
Verification
All at
3228b5954b, the merge oforigin/main0d43004100, afterpnpm install --frozen-lockfileandturbo run build --filter=@objectstack/cli...(60 tasks), then--filter=!@objectstack/docs(74 tasks). Each run went throughscripts/pm/os-verify-lock.sh, and each readsVERDICT command-exit 0.pnpm --filter @objectstack/verify exec vitest run --maxWorkers=2:Test Files 37 passed (37),Tests 299 passed (299).dashboard-window.test.tsalone:Tests 10 passed (10).pnpm --filter @objectstack/verify typecheck && pnpm --filter @objectstack/cli typecheck:check:test-typecheck: OKfor both. Withtsc --listFilesOnly -p tsconfig.test.json,dashboard-windowis in verify's program (2 hits) andverify-dashboard-windowsin cli's (1 hit).pnpm --filter @objectstack/cli exec vitest run --project integration --maxWorkers=2over the eighttest/verify-*.test.tsfiles: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 pinsecond-object-read-exposure.pin.test.tsis among them, with noCALLERSrow needed: the family never callsreferenceTargetOf.pnpm check:verify-stand-inexited 1 before the twoNOT_A_STAND_INrows (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).node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackwith no paths derived 115 commands, the same 115 as over this PR's 9 paths. 115 run, all exit 0, and--ranreports115 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.eslint --no-inline-config --format jsonover the 6 touched source files reads 6 files, 0 errors, 0 warnings, counted from the JSON. Population:--print-configresolves a config for all 6; the three Markdown files answer "File ignored because no matching configuration was supplied". Invariance:eslint.config.mjsnever enables type-aware linting (its comment at about:327; noparserOptions.project), so this diff cannot move an untouched file's verdict.3228b5954b: the same counts as the table above.Acceptance notes
{today}(objectuidashboard-filtersPRESET_RANGES, at the.objectui-shapin7578323a), while the spec'sDATE_RANGE_PRESET_MACRO_WINDOWSleaves 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 andcli.mdxsay so.created_atas 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.fixtureBody,vary,laterBy,uniqueFields) are now copied in four family modules, each module-private, as slice 4's record noted; a shared one belongs inderive.ts, outside this claim.Deviations
DATE_RANGE_PRESET_MACRO_WINDOWS, not hotcrm's{ $gte: '{N_days_ago}', $lte: '{today}' }. That pair is the console's own lowering (objectuiPRESET_RANGES), a table in the sibling repo; copying it here would be a fourth copy of the vocabularydate-range-presets.tswas 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'sresolveAnalyticsDateRangePreset) was not used: it needs a dataset dimension, while a dashboard window binds an object field.403reading the object, so the family takes the caller's token (os verifypassessignIn()'s), as the CRUD, reference-shape, delete-behaviour and unique-scope families do. Fixture rows are owned by it.inconclusive, never contradicted.bootStackandos servecompose identically, so the same control runs from the spawned CLI.$nin/$neare 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.won_dealsneeds it.^17.7.0against 18). The 76email_template.categorytombstones were removed in memory.origin/mainwas merged once, at0d43004100, and the build was refreshed; one conflict incli.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.mainthen moved 6 more commits, tod206d08cab, none on this PR's files. One,f931e765b7, refinesDashboardHeaderActionSchema.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 declaredapisanswer 404 ENDPOINT_NOT_FOUND there whileos serveof 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.Generated by Claude Code