Skip to content

Commit ba8bf1d

Browse files
fix(showcase): the Status × Priority matrix subtitle names the estimated hours its cells hold (#24084)
Fixes #23931 Clause-②: no. The text of an example app's report changes. No export, type or accept set moves. ## What The showcase report `showcase_status_priority_matrix` ("Status × Priority (Matrix)") showed the subtitle "Task counts cross-tabulated by status and priority." Its cells are `values: ['est_hours']`, which is a `sum` of `estimate_hours`. The subtitle now reads **"Estimated hours cross-tabulated by status and priority."** The measure, the dataset and the renderer are unchanged. ## Why hours and not counts The subtitle was wrong. The measure was not. - `est_hours` is declared in `examples/app-showcase/src/ui/datasets/chart-gallery.dataset.ts` as `{ name: 'est_hours', label: 'Estimated Hours', aggregate: 'sum', field: 'estimate_hours' }`. - The matrix has summed hours since the report was first written. At `e136adaaeb`, the commit that added the showcase, it had `columns: [{ field: 'estimate_hours', label: 'Hours', aggregate: 'sum' }]` next to the same "Task counts" text. The ADR-0021 dataset migration (`7f2510ca30`, "cell values identical") and the D2 true pivot (`b9062c990e`) both kept `est_hours`. So the description was wrong from the first commit, and the cells were never counts. - A count matrix over the same two dimensions already exists. The Chart Gallery's `pivot_tasks` widget (`src/ui/dashboards/chart-gallery.dashboard.ts:93`) has `dimensions: ['status', 'priority'], values: ['task_count']`. Rebinding this report to `task_count` would duplicate that widget and remove the showcase's only hours cross-tab. - The sibling reports already use this wording: "Estimated hours grouped by task status." and "Estimated hours by status, plotted as a bar chart." ## Translation bundle There is nothing to update. Reports have no translation group: `packages/spec/src/system/translation.zod.ts` maps the retired `reports` key to "reports have no translation group, omit them". The showcase bundle `src/system/translations/index.ts` has no report entry. A repo-wide grep finds the old string only at `src/ui/reports/index.ts:29`. ## Pin `examples/app-showcase/test/report-description-measure.test.ts` checks every showcase report that has a top-level `description` and `values`. It reads the bound dataset's own declarations. It does not compare the subtitle against a copy of the expected text. 1. The description must name a bound measure by its declared `label`, which is the name the cell header shows. For a `count` measure, count words are also accepted. 2. The description may speak of counts only when a bound measure is a `count`. 3. An anti-vacuity check: the matrix must be among the graded reports. Joined reports bind measures per block and have no top-level `values`, so the filter leaves them out. No test is skipped. The test reads its subject from source (`../src/ui/reports/index.js`), so no build sits between a mutation and the run. **Ablation (one-off; no permanent file).** On the committed fix at `93d1e79c8d`, `scripts/ablation-replace.mjs` put today's text back and ran the file: - Mutation on disk: anchor 1 to 0, replacement 0 to 1, blob `28b7ce4486cc` to `1054fbd9a08b`. - Red, as expected: `× showcase_status_priority_matrix: the description names a bound measure` and `× showcase_status_priority_matrix: the description claims no count its cells do not hold`, giving `Tests 2 failed | 5 passed (7)`. - Restore proven: blob equals HEAD (`28b7ce4486cc`) and `git diff HEAD` is empty. ## Verification at `103c1b6e7d` (after merging `origin/main` `8133cc7805`) - `pnpm --filter @objectstack/example-showcase typecheck` exits 0. `tsc --listFiles` includes the new test file (1 hit). - `pnpm --filter @objectstack/example-showcase exec vitest run --maxWorkers=2` exits 0: `Test Files 34 passed (34)`, `Tests 422 passed (422)`. - `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` derives 42 commands. All 42 exited 0 on this head. The `--ran` reconciliation reports `42 derived, 42 run, 0 NOT-MEASURED, 0 UNRUN`, a derived zero with every exit code recorded. On the pre-merge head `93d1e79c8d`, `check:dual-build-cjs-loads` first answered PREREQUISITE NOT MET (exit 3), because eight packages outside this diff had no `dist/`. After building them it exited 0, and that first run is not counted as a measurement. - `test:repo` was not run locally. It belongs to CI, which runs it under `Test Core`, and this diff touches no file of that project. ## Scope and exclusions - The dataset (`chart-gallery.dataset.ts`) and the renderer are not touched. Both are correct. - There is no changeset: `@objectstack/example-showcase` is `"private": true` and publishes nothing, so this PR carries `skip-changeset`. ## Acceptance notes - The pin checks report-level subtitles only. Block labels inside the joined report (`Open Tasks` binds `est_hours`, `Completed Tasks` binds `task_count`) name a population, not a quantity, so the pin does not grade them. This is a boundary note, not a defect claim. --- _Generated by [Claude Code](https://claude.ai/code/session_018VqJPGA1NDNSCZsnecsChP)_ Co-authored-by: Claude <noreply@anthropic.com>
1 parent 76d9dca commit ba8bf1d

2 files changed

Lines changed: 85 additions & 1 deletion

File tree

‎examples/app-showcase/src/ui/reports/index.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ export const HoursByStatusReport = defineReport({
2626
export const StatusPriorityMatrixReport = defineReport({
2727
name: 'showcase_status_priority_matrix',
2828
label: 'Status × Priority (Matrix)',
29-
description: 'Task counts cross-tabulated by status and priority.',
29+
description: 'Estimated hours cross-tabulated by status and priority.',
3030
type: 'matrix',
3131
drilldown: true,
3232
// ADR-0021 D2 — true pivot: `rows` down × `columns` across, measures in cells.
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
2+
3+
import { describe, it, expect } from 'vitest';
4+
5+
import * as datasets from '../src/ui/datasets/index.js';
6+
import { allReports } from '../src/ui/reports/index.js';
7+
8+
/**
9+
* A report's `description` is the subtitle the console renders under its
10+
* title; its cells are the measures `values` binds. The two must name the same
11+
* quantity. The Status × Priority matrix shipped "Task counts cross-tabulated
12+
* by status and priority." over `values: ['est_hours']` — a `sum` of
13+
* `estimate_hours` — so a reader trusting the subtitle took a cell reading
14+
* 60.0 for sixty tasks when it held one task's estimated hours (#23931).
15+
*
16+
* Graded against the bound DATASET's own declarations, never against a copy of
17+
* the subtitle text:
18+
*
19+
* - a bound measure is named by its declared `label` — the name the cell's
20+
* header shows — and a `count` measure may also be named in count words;
21+
* - the converse is the defect as shipped: count words in a subtitle whose
22+
* bound measures count nothing.
23+
*
24+
* Joined reports bind their measures per block and carry no top-level
25+
* `values`, so their subtitle names no single quantity; they are out of scope
26+
* by construction, not by a skip.
27+
*/
28+
29+
type Measure = { name: string; label?: unknown; aggregate?: string };
30+
31+
const MEASURES_BY_DATASET = new Map<string, readonly Measure[]>(
32+
Object.values(datasets).map((d) => [d.name, d.measures as readonly Measure[]]),
33+
);
34+
35+
/** The words a subtitle uses to say its cells are counts. */
36+
const COUNT_WORDS = /\b(counts?|number of)\b/i;
37+
38+
/** The plain-string copy this pin knows how to read; an inline locale map is reported, not guessed. */
39+
function plain(value: unknown, what: string): string {
40+
expect(typeof value, `${what} must be a plain string for this pin to read it`).toBe('string');
41+
return value as string;
42+
}
43+
44+
function boundMeasures(report: (typeof allReports)[number]): Measure[] {
45+
const measures = MEASURES_BY_DATASET.get(report.dataset ?? '');
46+
expect(measures, `${report.name}: dataset '${report.dataset}' is not a showcase dataset`).toBeDefined();
47+
return (report.values ?? []).map((name) => {
48+
const measure = measures!.find((m) => m.name === name);
49+
expect(measure, `${report.name}: measure '${name}' is not declared on '${report.dataset}'`).toBeDefined();
50+
return measure!;
51+
});
52+
}
53+
54+
const graded = allReports
55+
.filter((r) => r.description !== undefined && (r.values?.length ?? 0) > 0)
56+
.map((r) => [r.name, r] as const);
57+
58+
describe('showcase reports — the subtitle names what the cells hold', () => {
59+
it('grades the Status × Priority matrix, among the measure-bound reports', () => {
60+
expect(graded.map(([name]) => name)).toContain('showcase_status_priority_matrix');
61+
});
62+
63+
it.each(graded)('%s: the description names a bound measure', (name, report) => {
64+
const description = plain(report.description, `${name}.description`).toLowerCase();
65+
const named = boundMeasures(report).filter((m) => {
66+
const label = plain(m.label, `measure ${m.name}.label`).toLowerCase();
67+
return description.includes(label) || (m.aggregate === 'count' && COUNT_WORDS.test(description));
68+
});
69+
expect(
70+
named.map((m) => m.name),
71+
`${name}: "${report.description}" names none of its bound measures ${JSON.stringify(report.values)}`,
72+
).not.toEqual([]);
73+
});
74+
75+
it.each(graded)('%s: the description claims no count its cells do not hold', (name, report) => {
76+
const description = plain(report.description, `${name}.description`);
77+
const bindsACount = boundMeasures(report).some((m) => m.aggregate === 'count');
78+
const countWord = description.match(COUNT_WORDS)?.[0];
79+
expect(
80+
countWord === undefined || bindsACount,
81+
`${name}: "${description}" says "${countWord}", but ${JSON.stringify(report.values)} aggregate no count`,
82+
).toBe(true);
83+
});
84+
});

0 commit comments

Comments
 (0)