zz-meridian 0.18.1 → 0.19.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/migrations.js +8 -0
- package/dist/ownership.js +1 -1
- package/package.json +1 -1
- package/payload/CHANGELOG.md +26 -0
- package/payload/docs/data-display.md +2 -2
- package/payload/docs/distribution.md +2 -2
- package/payload/docs/testing.md +1 -1
- package/payload/package.json +1 -1
- package/payload/scripts/interactions.ts +11 -9
- package/payload/scripts/lib/budgets.ts +5 -1
- package/payload/scripts/lib/sizes.ts +24 -0
- package/payload/scripts/sizes.ts +56 -32
- package/payload/scripts/verify.baseline.json +26 -0
- package/payload/scripts/verify.config.ts +4 -12
- package/payload/scripts/verify.ts +9 -2
- package/payload/skills/zz-meridian/references/existing-project.md +2 -2
- package/payload/skills/zz-meridian/references/ownership.md +1 -1
- package/payload/skills/zz-meridian/references/standard.md +3 -2
- package/payload/skills/zz-meridian/references/update.md +3 -2
- package/payload/skills/zz-meridian/references/validation.md +1 -1
- package/payload/src/components/charts/column-chart/README.md +9 -7
- package/payload/src/components/charts/column-chart/index.tsx +23 -12
- package/payload/src/components/charts/column-chart/preview.tsx +12 -1
- package/payload/src/components/patterns/activity-feed/README.md +2 -2
- package/payload/src/components/patterns/activity-feed/index.tsx +5 -3
- package/payload/src/components/patterns/activity-feed/preview.tsx +2 -2
- package/payload/src/components/patterns/data-table/README.md +1 -1
- package/payload/src/components/patterns/data-table/index.tsx +4 -1
- package/payload/src/components/patterns/data-table/preview.tsx +2 -1
- package/payload/src/components/patterns/rail/README.md +1 -1
- package/payload/src/components/patterns/rail/index.tsx +12 -2
- package/payload/src/components/ui/button/README.md +2 -2
- package/payload/src/components/ui/button/index.tsx +9 -6
- package/payload/src/styles/base.css +14 -0
- package/payload/src/views/console-chrome.tsx +4 -1
- package/payload/tests/activity-feed-link.test.tsx +23 -0
- package/payload/tests/button-busy.test.tsx +38 -0
- package/payload/tests/column-chart-no-data.test.tsx +5 -1
- package/payload/tests/column-chart-stacked.test.tsx +23 -0
- package/payload/tests/data-table.test.tsx +11 -0
- package/payload/tests/rail-current-held.test.tsx +32 -0
- package/payload/tests/verify-sizes.test.ts +11 -2
package/dist/migrations.js
CHANGED
|
@@ -462,6 +462,14 @@ export const RELEASE_MIGRATIONS = [
|
|
|
462
462
|
instructions: 'Bring `tests/verify-suite-outcome.test.ts` over from the release as it has it. A file of your own that calls `finalOutcome` passes the tiers, `finalOutcome({ full, timed }, notRun)`, where it passed whether the run was full (`finalOutcome(true, notRun)`): `references/validation.md` describes the tiers, `pnpm verify`, `--full` and `--timed`.',
|
|
463
463
|
checks: ['gate'],
|
|
464
464
|
},
|
|
465
|
+
{
|
|
466
|
+
id: 'no-data-mark',
|
|
467
|
+
since: '0.19.0',
|
|
468
|
+
applies: (root) => matching(root, (rel, src) => importsOf(rel, src).some(({ from }) => from === 'src/components/charts/column-chart') && /\.bg-fill-track['"]\)\)\.not\.toBeNull\(\)/.test(src)),
|
|
469
|
+
summary: 'A Column chart\'s period with no data draws a dashed hairline on the baseline, where it drew a `fill-track` track the height of the scale.',
|
|
470
|
+
instructions: 'Bring `tests/column-chart-no-data.test.tsx` over from the release as it has it. Since 0.19.0 `src/components/charts/column-chart/index.tsx` draws no column for a `null` period: its box has no height, a dashed hairline (`data-no-data-mark`) keeps the slot, and the word stands over it, because the track was the tallest mark on the chart. A test of your own that finds `.bg-fill-track` or `[data-track]` in a no-data column finds `[data-no-data-mark]` instead.',
|
|
471
|
+
checks: ['gate'],
|
|
472
|
+
},
|
|
465
473
|
];
|
|
466
474
|
/**
|
|
467
475
|
* A test of the rail that stands over a fake `ResizeObserver` answering `observe` with nothing (0.16.2). The lookbehind
|
package/dist/ownership.js
CHANGED
|
@@ -7,7 +7,7 @@ export const FIXED = [...LIB, 'src/lib/assistant/prompt.ts', 'src/views/console-
|
|
|
7
7
|
/** The one rule, a plain-English line per clause. `references/ownership.md` renders it, so the guidance cannot drift. */
|
|
8
8
|
export const OWNERSHIP = [
|
|
9
9
|
'Managed: the payload files under `tokens/`, `src/styles/` and `src/components/`, except each component\'s `README.md` and `preview.tsx`.',
|
|
10
|
-
'Managed: the distributed scripts under `scripts/`, except `scripts/verify.config.ts`, the
|
|
10
|
+
'Managed: the distributed scripts under `scripts/`, except `scripts/verify.config.ts`, the size baseline `optional:scripts/verify.baseline.json` (first-load JS and the HTML of each page) and a team-local `optional:scripts/check.local.ts`.',
|
|
11
11
|
'Managed: the library helpers `src/lib/cn.ts`, `src/lib/format.ts`, `src/lib/format-date.ts`, `src/lib/period.ts`, `src/lib/color.ts`, `src/lib/host.ts`, `src/lib/preferences.ts`, `src/lib/csv.ts`, `src/lib/safe-markdown.ts`, `src/lib/logo.ts`, `src/lib/collection.ts`, `src/lib/explore.ts`, `src/lib/live.ts`, `src/lib/shared-context.ts`, `src/lib/agent-guidance.ts`, `src/lib/insight.ts` and `src/lib/facts.ts`, and the assistant prompt `src/lib/assistant/prompt.ts`.',
|
|
12
12
|
'Managed: `src/views/console-chrome.tsx` and `tests/setup.ts`.',
|
|
13
13
|
'Managed: `optional:scripts/package.json`, only when the adopt or create that set the project up generated it for this project shape.',
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "zz-meridian",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.19.1",
|
|
4
4
|
"description": "Bring ZZ Meridian, a dashboard design system, into a Next.js project, or start a new dashboard on it. Copies the files in; nothing depends on this package at runtime.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
package/payload/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,32 @@
|
|
|
2
2
|
|
|
3
3
|
Every release of ZZ Meridian, newest first. Versions follow semver: a removed or renamed token, prop or card is major; a new card, token or variant is minor; a corrected value is a patch. Each entry says what breaks and what to do instead.
|
|
4
4
|
|
|
5
|
+
## [0.19.1] · 2026-10-19
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- **The presses suite no longer crashes when a press replaces the document late** ([#64](https://github.com/zhixuan312/zz-meridian/issues/64)). A control whose answer reloads the page once a request is accepted, a team switch that has the server draw the new team's chrome, could replace the document after `scripts/interactions.ts` had looked for an answer; its next read of the page then found no helper and stopped the whole suite with `ReferenceError: deepQuery is not defined`. A document replaced after the look is a press that moved the page: the suite opens the route again and presses the next control, and the press is not reported as doing nothing.
|
|
10
|
+
|
|
11
|
+
## [0.19.0] · 2026-10-19
|
|
12
|
+
|
|
13
|
+
### Breaking
|
|
14
|
+
|
|
15
|
+
- **Every page's HTML is held to its own baseline, and measured gzipped** ([#57](https://github.com/zhixuan312/zz-meridian/issues/57), [#56](https://github.com/zhixuan312/zz-meridian/issues/56)). `scripts/sizes.ts` measures a page's HTML as a person downloads it, the complete streamed response gzipped, where it measured the uncompressed bytes of the routes in `budgets.htmlKb` only. `node scripts/sizes.ts --write-baseline --base <url>`, against the running build, records the HTML of every page (no API, no dynamic segment, not the not-found screen) beside the first-load figures in `scripts/verify.baseline.json`, and `pnpm verify` holds each to `budgets.htmlGrowthPct` (5%) over its own figure, as first-load JS is held. A growth you accept is recorded once, with the same command, rather than by editing numbers. `budgets.htmlKb` stays as an optional absolute cap, now in gzipped KiB: a cap set on uncompressed bytes is several times too loose, so lower it to the gzipped figure `pnpm verify` prints, or remove it and rely on the baseline. Until the baseline holds `html`, the HTML line is a warning, and `pnpm verify --full` refuses, naming the command. Under `partialPrefetching` Next.js writes each shell's static trees into a page's HTML twice; gzipped, the second copy costs a few KiB rather than tens, so the figure follows the page.
|
|
16
|
+
|
|
17
|
+
### New
|
|
18
|
+
|
|
19
|
+
- **A stacked Column chart's part can take a categorical slot** ([#61](https://github.com/zhixuan312/zz-meridian/issues/61)). `ColumnPart.color` (1 to 6) draws the part in `series-<n>`, as `TrendSeries.color` does, for parts that are identities, such as teams, or for two or more usual outcomes, which one neutral would make one. A part with no slot, Other among them, is neutral, and status colours stay out of such a chart: coral and saffron read as a warning beside a real one. `docs/data-display.md`'s Parts of one whole says when to use it.
|
|
20
|
+
- **`DataTable` takes `retrying`** ([#59](https://github.com/zhixuan312/zz-meridian/issues/59)): its error state's Retry is busy while the person's retry is on its way. Pass it for their own press only.
|
|
21
|
+
- **`ActivityEvent.href`** ([#63](https://github.com/zhixuan312/zz-meridian/issues/63)) links an event's object to its record, with the arrow that says it stays in the product.
|
|
22
|
+
- **`Rail` takes `declared`**, every destination the product declares; `ConsoleRail` passes it from the whole navigation.
|
|
23
|
+
|
|
24
|
+
### Fixed
|
|
25
|
+
|
|
26
|
+
- **A busy Button keeps its focus** ([#58](https://github.com/zhixuan312/zz-meridian/issues/58)). It was `disabled`, so a focused button that turned busy from anything but its own press, a Retry whose read a live hint or a poll started, lost focus to the page and the next Tab began again at the top. It is `aria-disabled` now, stays in the tab order, and drops a press while busy, a submit's included; `aria-busy` and the spinner are unchanged. A test of yours that expects a busy button to be `disabled` expects `aria-disabled="true"`.
|
|
27
|
+
- **A Column chart's period with no data is no longer its tallest mark** ([#60](https://github.com/zhixuan312/zz-meridian/issues/60)). It drew a `fill-track` track the height of the scale, which in light theme read as the biggest period. It draws no column now, a dashed hairline just above the baseline, and its word ("No data") stands over it where a value would. The hairline carries `data-no-data-mark` where the track carried `data-track`. `npx zz-meridian@latest update` lists `migration:no-data-mark` for a project whose copy of `tests/column-chart-no-data.test.tsx` still expects the track: bring the release's over.
|
|
28
|
+
- **The rail lights nothing for an address under a destination the person does not hold** ([#62](https://github.com/zhixuan312/zz-meridian/issues/62)). It lit the longest destination they held, so someone refused `/admin/audit` saw `/admin` lit. The current page is found among every declared destination and lit only when their rail holds it. A product that passed `current=''` to work around it can stop.
|
|
29
|
+
- **A Button's class list is 178 bytes, not 394** ([#56](https://github.com/zhixuan312/zz-meridian/issues/56)): the frame every button shares is the `.btn` class in `src/styles/base.css`, so a table of row actions does not repeat it on every instance.
|
|
30
|
+
|
|
5
31
|
## [0.18.1] · 2026-10-19
|
|
6
32
|
|
|
7
33
|
### Changed
|
|
@@ -36,7 +36,7 @@ Two measures of different size are two charts on one Meridian, not one chart wit
|
|
|
36
36
|
- **Magnitude**: one hue, light to dark, built from the accent and the surface (the heatmap).
|
|
37
37
|
- **Magnitude by rank**: a Heatmap's step is a cell's share of the largest cell (`max`) unless one outlier would leave every other cell at the lowest step. Then it is ranked (`scale="quantile"`): the cells above zero take five steps of equal count. A step no longer says how much, so the legend's upper end reads the value the top step starts at ("900 or more") in place of "More", and the readout and the hidden table give each cell's exact value.
|
|
38
38
|
- **State**: positive, warning, critical, always with a word or an icon.
|
|
39
|
-
- **Parts of one whole**: one hue. The parts share the accent, mixed into the surface, deepest at the bottom and paler upward (the stacked Trend chart and the stacked Column chart take the same mix), with a 1px hairline of the card between neighbours and at most four parts, the fourth being Other. When the parts are outcomes they carry a tone instead: the usual outcome is neutral (`chart-neutral-strong`), one that needs somebody is warning or critical, the accent never stands beside a tone, and the positive colour is never the usual case.
|
|
39
|
+
- **Parts of one whole**: one hue. The parts share the accent, mixed into the surface, deepest at the bottom and paler upward (the stacked Trend chart and the stacked Column chart take the same mix), with a 1px hairline of the card between neighbours and at most four parts, the fourth being Other. When the parts are outcomes they carry a tone instead: the usual outcome is neutral (`chart-neutral-strong`), one that needs somebody is warning or critical, the accent never stands beside a tone, and the positive colour is never the usual case. Parts that are identities (teams), or two or more usual outcomes that one neutral would make one, take the categorical slots instead, in order (`series-1` onward, as Identity above), and a part with no slot is neutral. Status colours stay out of such a chart, as the accent stays out of a toned one, since coral and saffron read as a warning beside a real one: a chart with an outcome that needs somebody is a toned chart.
|
|
40
40
|
|
|
41
41
|
Text never wears a series colour; values and labels stay in ink.
|
|
42
42
|
|
|
@@ -68,7 +68,7 @@ A period still in progress is counted in no total, change or finding. A figure f
|
|
|
68
68
|
- The figure is set tight (the hero figure at `tracking-display`, a card's figure at -0.03em); its unit steps down to half size in `ink-3`; money steps its cents down; compact counts keep their decimal (`2.9M`).
|
|
69
69
|
- Tabular figures where numbers stack or change in place (tables, axes, readouts, and a tile's or the Featured metric's figure, which changes as the Meridian moves); proportional for a standalone figure that stays put.
|
|
70
70
|
- A missing value is a dash, never zero. Every figure names its period and unit somewhere visible.
|
|
71
|
-
- A period with no data is a gap in a chart, never a zero and never left out: a `null` keeps its place on the time axis. A Trend chart's and a Sparkline's line breaks at it; a Column chart's column keeps its slot
|
|
71
|
+
- A period with no data is a gap in a chart, never a zero and never left out: a `null` keeps its place on the time axis. A Trend chart's and a Sparkline's line breaks at it; a Column chart's column keeps its slot with a dashed hairline on the baseline, never a mark that stands up from it, and reads "No data" (or the product's word, `noData`) at rest, pointed at and in the table, and the scale leaves it out.
|
|
72
72
|
- A Heatmap of hours names the timezone its hours are cut in, in its readout, its announcement and its table: UTC unless the product passes `timezone`. A product that cuts its days at local midnight cuts its hours there too and names the zone, so a local Monday stays in Monday's row. The template's Usage passes the product's reporting timezone, `app.timezone`, as its Heatmap's `timezone` and names it in its cards and its context, so a product that sets that zone keeps its hours cut in it.
|
|
73
73
|
- A delta's arrow carries direction; its colour carries the judgement, which depends on the measure (latency down is good).
|
|
74
74
|
- A change says what it is against, in words beside it: "vs previous period" unless the caller says otherwise ("vs the week before"). The Featured metric and the Metric tile take the words as `compare`. On a wall of compact tiles the card that holds the wall says it once, and a tile draws the change's figure alone.
|
|
@@ -148,10 +148,10 @@ Two environment variables exist for the updater's own tests and are used only wh
|
|
|
148
148
|
reports as `migration:<id>` because it cannot make them. An entry applies when the project's recorded version is older
|
|
149
149
|
than its `since` and the target is not, and its `applies(root)` returns the paths that still have the old shape, or
|
|
150
150
|
null. It only reads, and only team-owned files (the ownership rule's managed paths are skipped); a file it cannot read is
|
|
151
|
-
absent. A project already on the new shape gets no line. Every entry carries `gate` and `build` as its checks (`verify-modes`, `agent-reads-section`, `nav-needs`, `agent-reads-own-group`, `agent-does-here-section`, `marker-measured-on-observe`, `view-page-matched-as-next`, `journey-moved`, `today-in-timezone` and `
|
|
151
|
+
absent. A project already on the new shape gets no line. Every entry carries `gate` and `build` as its checks (`verify-modes`, `agent-reads-section`, `nav-needs`, `agent-reads-own-group`, `agent-does-here-section`, `marker-measured-on-observe`, `view-page-matched-as-next`, `journey-moved`, `today-in-timezone`, `verify-tiers` and `no-data-mark`, `gate` alone), and its
|
|
152
152
|
instructions name the section of a reference (`cache.md`, `live.md`, `validation.md`, `agents.md`, `customize.md`), of the Start guide or of `docs/assistant.md` to follow.
|
|
153
153
|
|
|
154
|
-
|
|
154
|
+
Sixteen arrived after 0.5.0 and are resolved as the skill's `references/update.md` says: `share-view-context` and `agent-reads-section` (0.9.0), `assistant-view-tools` (0.10.0), `nav-needs` (0.13.0), `agent-reads-own-group` (0.15.0), `refusal-declared`, `managed-exports-removed`, `view-tool-page-explore`, `agent-does-here-section` and `sample-pages-renamed` (0.16.0), `marker-measured-on-observe` (0.16.2), `view-page-matched-as-next`, `journey-moved` and `today-in-timezone` (0.16.3), `verify-tiers` (0.17.0), and `no-data-mark` (0.19.0). The ten of 0.5.0 are:
|
|
155
155
|
|
|
156
156
|
| Id | Reported when a team-owned file still has | Completion step |
|
|
157
157
|
|----|-------------------------------------------|-----------------|
|
package/payload/docs/testing.md
CHANGED
|
@@ -125,7 +125,7 @@ Every check Meridian runs has one row, named by a key: a gate step, a group of t
|
|
|
125
125
|
| `verify/build` | large | functional | change | no | `pnpm verify` | 9.6 s at 40d29c7 | One production build. |
|
|
126
126
|
| `verify/route policy` | large | non-functional | change | no | `pnpm verify` | 0.1 s at 40d29c7 | Every route is declared static or dynamic as its policy says. |
|
|
127
127
|
| `verify/first-load` | large | non-functional | change | no | `pnpm verify` | 0.3 s at 40d29c7 | The first-load size of each route is within its budget. |
|
|
128
|
-
| `verify/html` | large | non-functional | change | no | `pnpm verify` |
|
|
128
|
+
| `verify/html` | large | non-functional | change | no | `pnpm verify` | 1.5 s at dbf4106 | Every page's served HTML, gzipped, is within `htmlGrowthPct` of its own baseline figure and under any `htmlKb` cap. |
|
|
129
129
|
| `verify/navigation` | large | functional | change | no | `pnpm verify` | 92.5 s at 269d307 | From a press in the rail, each route's shell, data and first interaction arrive, on desktop and through the phone drawer, judged on function only, never on time; up to three routes, every rail route under `--full` or `--timed`. |
|
|
130
130
|
| `verify/timing` | large | performance | timed | yes | `pnpm verify --timed` | 88.2 s at fa5bbff | The same navigation of every rail route, with each shell, data and interaction time judged against its budget. |
|
|
131
131
|
| `verify/personas` | large | functional | on request | no | `pnpm verify --as viewer` | 937.8 s at fa5bbff (the five personas) | Signed in as each named persona, the rail shows the routes that persona may open and a denied route draws its refusal. |
|
package/payload/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "zz-meridian-template",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.19.1",
|
|
4
4
|
"private": true,
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"description": "ZZ Meridian: a dashboard design system and starter. Six layers, from DTCG tokens through four layers of React components to product workspaces, and a Design Atlas, in a light and a dark theme.",
|
|
@@ -125,7 +125,8 @@ for (const width of WIDTHS) {
|
|
|
125
125
|
if (status >= 400) failures.push(`${route}: link ${href}${path === href ? '' : ` (${path})`} answers ${status}`);
|
|
126
126
|
}
|
|
127
127
|
for (let i = 0; i < controls.length; i++) {
|
|
128
|
-
|
|
128
|
+
// A document replaced since the last read has no helper: that is a page to open again, never a crash (#64).
|
|
129
|
+
let armed = await page.eval<{ x: number; y: number; covered?: string } | null>(ARM(i)).catch(() => null);
|
|
129
130
|
// Something left open or a toast in the way: start the page again and try once more.
|
|
130
131
|
if (!armed || armed.covered) { await fresh(); armed = await page.eval(ARM(i)); }
|
|
131
132
|
// Tagged by this suite a moment ago and gone after a fresh load: say so, never skip it.
|
|
@@ -150,14 +151,15 @@ for (const width of WIDTHS) {
|
|
|
150
151
|
if (!chosen) r = await look().catch(() => ({ changes: 0, moved: true }));
|
|
151
152
|
}
|
|
152
153
|
if (chosen) continue;
|
|
153
|
-
if (
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
}
|
|
154
|
+
if (r.moved) { await fresh(); continue; }
|
|
155
|
+
await page.send('Input.dispatchKeyEvent', { type: 'keyDown', key: 'Escape', code: 'Escape', windowsVirtualKeyCode: 27 });
|
|
156
|
+
await page.send('Input.dispatchKeyEvent', { type: 'keyUp', key: 'Escape', code: 'Escape', windowsVirtualKeyCode: 27 });
|
|
157
|
+
await sleep(200);
|
|
158
|
+
// A document replaced after the look closed (a team switch that reloads once its request is accepted) has lost the
|
|
159
|
+
// helper and the counters: the press moved the page, late, and the route is opened again (#64).
|
|
160
|
+
const after = await page.eval<{ replaced: boolean; open: boolean }>(`typeof deepQuery !== 'function' ? { replaced: true, open: false } : { replaced: location.href !== window.__href, open: !!deepQuery('[role="dialog"],[role="alertdialog"],[role="menu"],[role="listbox"]', document.body) }`).catch(() => ({ replaced: true, open: false }));
|
|
161
|
+
if (!r.changes && !after.replaced) failures.push(`${route} @${width}: "${controls[i]}" does nothing when ${touch ? 'tapped' : 'pressed'}`);
|
|
162
|
+
if (after.replaced || after.open) await fresh();
|
|
161
163
|
}
|
|
162
164
|
if (page.errors.length) failures.push(`${route} @${width}: ${page.errors[0]}`);
|
|
163
165
|
}
|
|
@@ -26,6 +26,7 @@ export type Budgets = {
|
|
|
26
26
|
navigation: { warm: ReadinessBudget; cold: ReadinessBudget; afterLive: ReadinessBudget };
|
|
27
27
|
firstLoadKb: number;
|
|
28
28
|
firstLoadGrowthPct: number;
|
|
29
|
+
htmlGrowthPct: number;
|
|
29
30
|
htmlKb: Record<string, number>;
|
|
30
31
|
prefetchKb: { desktop: number; phoneClosed: number };
|
|
31
32
|
};
|
|
@@ -34,6 +35,7 @@ export type BudgetsConfig = {
|
|
|
34
35
|
navigation?: { warm?: ReadinessBudget; cold?: ReadinessBudget; afterLive?: ReadinessBudget };
|
|
35
36
|
firstLoadKb?: number;
|
|
36
37
|
firstLoadGrowthPct?: number;
|
|
38
|
+
htmlGrowthPct?: number;
|
|
37
39
|
htmlKb?: Record<string, number>;
|
|
38
40
|
prefetchKb?: { desktop: number; phoneClosed: number };
|
|
39
41
|
};
|
|
@@ -45,7 +47,7 @@ const COLD: ReadinessBudget = {
|
|
|
45
47
|
interactiveMs: { desktop: 2000, phone: 4000 },
|
|
46
48
|
};
|
|
47
49
|
|
|
48
|
-
/**
|
|
50
|
+
/** Each page's HTML is held to its own baseline; an absolute HTML cap is a project's route, so the defaults set none. */
|
|
49
51
|
export const DEFAULT_BUDGETS: Budgets = {
|
|
50
52
|
navigation: {
|
|
51
53
|
warm: {
|
|
@@ -58,6 +60,7 @@ export const DEFAULT_BUDGETS: Budgets = {
|
|
|
58
60
|
},
|
|
59
61
|
firstLoadKb: 820,
|
|
60
62
|
firstLoadGrowthPct: 5,
|
|
63
|
+
htmlGrowthPct: 5,
|
|
61
64
|
htmlKb: {},
|
|
62
65
|
prefetchKb: { desktop: 80, phoneClosed: 0 },
|
|
63
66
|
};
|
|
@@ -91,6 +94,7 @@ export function budgetsOf(config: BudgetsConfig): Budgets {
|
|
|
91
94
|
navigation: { warm, cold, afterLive: b.navigation?.afterLive ?? cold },
|
|
92
95
|
firstLoadKb: b.firstLoadKb ?? DEFAULT_BUDGETS.firstLoadKb,
|
|
93
96
|
firstLoadGrowthPct: b.firstLoadGrowthPct ?? DEFAULT_BUDGETS.firstLoadGrowthPct,
|
|
97
|
+
htmlGrowthPct: b.htmlGrowthPct ?? DEFAULT_BUDGETS.htmlGrowthPct,
|
|
94
98
|
htmlKb: b.htmlKb ?? DEFAULT_BUDGETS.htmlKb,
|
|
95
99
|
prefetchKb: b.prefetchKb ?? DEFAULT_BUDGETS.prefetchKb,
|
|
96
100
|
};
|
|
@@ -35,6 +35,30 @@ export function firstLoadProblems(
|
|
|
35
35
|
return { problems, growth: baseline ? 'checked' : 'not-configured' };
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
+
/**
|
|
39
|
+
* The pages whose HTML is held to a baseline: every page route but an API, one under a dynamic segment (its address
|
|
40
|
+
* needs an id the build does not know) and the not-found screen, which answers 404.
|
|
41
|
+
*/
|
|
42
|
+
export function htmlRoutes(routes: string[]): string[] {
|
|
43
|
+
return routes.filter((r) => !r.startsWith('/api/') && !r.includes('[') && r !== '/_not-found');
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Pages whose HTML grew past their allowance over the baseline, as first-load JS is held; without one growth is not configured. */
|
|
47
|
+
export function htmlGrowthProblems(
|
|
48
|
+
sizes: Record<string, number>,
|
|
49
|
+
{ growthPct, baseline }: { growthPct: number; baseline: Record<string, number> | null },
|
|
50
|
+
): { problems: string[]; growth: 'checked' | 'not-configured' } {
|
|
51
|
+
const problems: string[] = [];
|
|
52
|
+
for (const [route, base] of Object.entries(baseline ?? {})) {
|
|
53
|
+
const bytes = sizes[route];
|
|
54
|
+
if (bytes === undefined || !(base > 0)) continue;
|
|
55
|
+
const grew = ((bytes - base) / base) * 100;
|
|
56
|
+
if (grew > growthPct) problems.push(`${route}: HTML grew ${grew.toFixed(1)}% over its baseline (cap ${growthPct}%)`);
|
|
57
|
+
}
|
|
58
|
+
return { problems, growth: baseline ? 'checked' : 'not-configured' };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Pages over an absolute HTML cap, a route a project pins on top of its baseline. */
|
|
38
62
|
export function htmlProblems(sizes: Record<string, number>, capsKiB: Record<string, number>): string[] {
|
|
39
63
|
return Object.entries(capsKiB)
|
|
40
64
|
.filter(([route, cap]) => (sizes[route] ?? 0) > cap * KIB)
|
package/payload/scripts/sizes.ts
CHANGED
|
@@ -1,36 +1,42 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The size checks of a production build: first-load JS of every non-API route
|
|
3
|
-
*
|
|
2
|
+
* The size checks of a production build: first-load JS of every non-API route, and the HTML of every page, each against
|
|
3
|
+
* its own baseline.
|
|
4
4
|
*
|
|
5
5
|
* node scripts/sizes.ts [--base http://127.0.0.1:3000] [--write-baseline]
|
|
6
6
|
*
|
|
7
7
|
* First-load comes from the build's own `diagnostics/route-bundle-stats.json`, read once, with each chunk's size from
|
|
8
8
|
* disk: every chunk a route loads counted once, uncompressed. It is checked against `budgets.firstLoadKb` and, when
|
|
9
|
-
* `scripts/verify.baseline.json` exists, against `budgets.firstLoadGrowthPct` over it.
|
|
10
|
-
*
|
|
11
|
-
*
|
|
9
|
+
* `scripts/verify.baseline.json` exists, against `budgets.firstLoadGrowthPct` over it. A page's HTML is what a person
|
|
10
|
+
* downloads: the complete streamed response from the app running at `--base`, gzipped. Every page in the baseline's
|
|
11
|
+
* `html` is held to `budgets.htmlGrowthPct` over its own figure, and a route in `budgets.htmlKb` also to that absolute
|
|
12
|
+
* cap. Without a baseline a growth check is `not-configured`, a warning and never a pass; without `--base` the HTML is
|
|
13
|
+
* `not run`.
|
|
12
14
|
*
|
|
13
|
-
* `--write-baseline` records the build's
|
|
14
|
-
*
|
|
15
|
+
* `--write-baseline` records the build's sizes as the team's baseline, `scripts/verify.baseline.json`, and checks
|
|
16
|
+
* nothing: first-load for every route and, with `--base`, the HTML of every page (no API, no dynamic segment, not the
|
|
17
|
+
* not-found screen). Without `--base` the HTML baseline already there is kept. The file is the team's: commit it on its
|
|
18
|
+
* own, from a build whose sizes were reviewed.
|
|
15
19
|
*
|
|
16
20
|
* One line per check, and a JSON summary on the last line:
|
|
17
21
|
*
|
|
18
22
|
* ok|FAIL|warn|not run <what> ...
|
|
19
|
-
* sizes: {"status","firstLoad":{"status","routes","largest","growth","problems"},"html":{"status","sizes","reason","problems"}}
|
|
23
|
+
* sizes: {"status","firstLoad":{"status","routes","largest","growth","problems"},"html":{"status","sizes","growth","reason","problems"}}
|
|
20
24
|
*
|
|
21
25
|
* The exit code is non-zero on a FAIL and on a build or baseline that cannot be read.
|
|
22
26
|
*/
|
|
23
27
|
import fs from 'node:fs';
|
|
24
28
|
import path from 'node:path';
|
|
29
|
+
import { gzipSync } from 'node:zlib';
|
|
25
30
|
|
|
26
31
|
import { budgetsOf, type BudgetsConfig } from './lib/budgets.ts';
|
|
27
32
|
import { distDirOf } from './lib/next-config.ts';
|
|
28
|
-
import { firstLoad, firstLoadProblems, htmlProblems } from './lib/sizes.ts';
|
|
33
|
+
import { firstLoad, firstLoadProblems, htmlGrowthProblems, htmlProblems, htmlRoutes } from './lib/sizes.ts';
|
|
29
34
|
import verifyConfig from './verify.config.ts';
|
|
30
35
|
|
|
31
36
|
const ROOT = path.resolve(import.meta.dirname, '..');
|
|
32
37
|
const BASELINE = path.join(ROOT, 'scripts/verify.baseline.json');
|
|
33
38
|
const METHOD = 'route-bundle-stats firstLoadChunkPaths, deduplicated, uncompressed bytes';
|
|
39
|
+
const HTML_METHOD = 'the complete streamed response, gzipped (zlib level 6), bytes';
|
|
34
40
|
const KIB = 1024;
|
|
35
41
|
const kib = (bytes: number): number => Math.round(bytes / KIB);
|
|
36
42
|
|
|
@@ -56,29 +62,32 @@ function measured(dist: string): { route: string; bytes: number }[] {
|
|
|
56
62
|
} catch (e) { return fail((e as Error).message); }
|
|
57
63
|
}
|
|
58
64
|
|
|
65
|
+
type Baseline = { firstLoad: Record<string, number>; html: Record<string, number> | null };
|
|
66
|
+
const SHAPE = '{ "method": string, "routes": { "<route>": <bytes> }, "html"?: { "method": string, "routes": { "<route>": <bytes> } } }';
|
|
67
|
+
const sized = (v: unknown): v is Record<string, number> => !!v && typeof v === 'object' && Object.values(v).every((n) => typeof n === 'number' && n > 0);
|
|
68
|
+
|
|
59
69
|
/** The team's baseline, or null when it has none. A file that is there and wrong is an error, never a missing baseline. */
|
|
60
|
-
function baselineOf():
|
|
70
|
+
function baselineOf(): Baseline | null {
|
|
61
71
|
if (!fs.existsSync(BASELINE)) return null;
|
|
62
72
|
const rel = path.relative(ROOT, BASELINE);
|
|
63
|
-
let raw: { routes?:
|
|
73
|
+
let raw: { routes?: unknown; html?: { routes?: unknown } };
|
|
64
74
|
try { raw = JSON.parse(fs.readFileSync(BASELINE, 'utf8')); } catch (e) { return fail(`${rel} is not JSON: ${(e as Error).message}`); }
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
return fail(`${rel} must be { "method": string, "routes": { "<route>": <bytes> } }; write it with node scripts/sizes.ts --write-baseline`);
|
|
75
|
+
if (!sized(raw?.routes) || (raw.html !== undefined && !sized(raw.html?.routes))) {
|
|
76
|
+
return fail(`${rel} must be ${SHAPE}; write it with node scripts/sizes.ts --write-baseline --base <url>`);
|
|
68
77
|
}
|
|
69
|
-
return routes as Record<string, number
|
|
78
|
+
return { firstLoad: raw.routes, html: raw.html ? (raw.html.routes as Record<string, number>) : null };
|
|
70
79
|
}
|
|
71
80
|
|
|
72
|
-
/**
|
|
73
|
-
async function htmlSizes(routes: string[]): Promise<{ sizes: Record<string, number>; problems: string[] }> {
|
|
81
|
+
/** Each page's complete response, read through the end of the stream and gzipped: what a person downloads. */
|
|
82
|
+
async function htmlSizes(routes: string[], why: string): Promise<{ sizes: Record<string, number>; problems: string[] }> {
|
|
74
83
|
const sizes: Record<string, number> = {};
|
|
75
84
|
const problems: string[] = [];
|
|
76
85
|
for (const route of routes) {
|
|
77
86
|
try {
|
|
78
|
-
const res = await fetch(base + route, { headers: { 'accept-encoding': 'identity' } });
|
|
79
|
-
const body = await res.arrayBuffer();
|
|
80
|
-
if (res.status !== 200) problems.push(`${route}:
|
|
81
|
-
else sizes[route] = body.byteLength;
|
|
87
|
+
const res = await fetch(base + route, { headers: { 'accept-encoding': 'identity' }, redirect: 'manual' });
|
|
88
|
+
const body = Buffer.from(await res.arrayBuffer());
|
|
89
|
+
if (res.status !== 200) problems.push(`${route}: ${why}, but the route answered ${res.status}`);
|
|
90
|
+
else sizes[route] = gzipSync(body).byteLength;
|
|
82
91
|
} catch (e) { problems.push(`${route}: could not be fetched from ${base} (${(e as Error).message})`); }
|
|
83
92
|
}
|
|
84
93
|
return { sizes, problems };
|
|
@@ -87,15 +96,23 @@ async function htmlSizes(routes: string[]): Promise<{ sizes: Record<string, numb
|
|
|
87
96
|
async function main(): Promise<number> {
|
|
88
97
|
const routes = measured(path.join(ROOT, await distDirOf(ROOT)));
|
|
89
98
|
if (args.includes('--write-baseline')) {
|
|
90
|
-
const
|
|
99
|
+
const kept = fs.existsSync(BASELINE) ? baselineOf()?.html ?? null : null;
|
|
100
|
+
let html: Record<string, number> | null = kept;
|
|
101
|
+
if (base) {
|
|
102
|
+
const got = await htmlSizes(htmlRoutes(routes.map((r) => r.route)), 'a page');
|
|
103
|
+
// A page that redirects or is refused here (a sign-in, a 404 by design) has no HTML of its own to hold.
|
|
104
|
+
for (const p of got.problems) console.log(`warn ${p}; left out of the HTML baseline`);
|
|
105
|
+
html = got.sizes;
|
|
106
|
+
}
|
|
107
|
+
const written = { method: METHOD, routes: Object.fromEntries(routes.map((r) => [r.route, r.bytes])), ...(html ? { html: { method: HTML_METHOD, routes: html } } : {}) };
|
|
91
108
|
fs.writeFileSync(BASELINE, JSON.stringify(written, null, 2) + '\n');
|
|
92
|
-
console.log(`ok wrote ${path.relative(ROOT, BASELINE)}: ${routes.length} routes`);
|
|
109
|
+
console.log(`ok wrote ${path.relative(ROOT, BASELINE)}: first-load of ${routes.length} routes, ${base ? `the HTML of ${Object.keys(html ?? {}).length} pages` : html ? 'the HTML baseline kept as it was (no --base to measure)' : 'no HTML (pass --base <url> of the running build to record it)'}`);
|
|
93
110
|
return 0;
|
|
94
111
|
}
|
|
95
112
|
|
|
96
113
|
const budgets = budgetsOf(verifyConfig as BudgetsConfig);
|
|
97
114
|
const baseline = baselineOf();
|
|
98
|
-
const size = firstLoadProblems(routes, { capKiB: budgets.firstLoadKb, growthPct: budgets.firstLoadGrowthPct, baseline });
|
|
115
|
+
const size = firstLoadProblems(routes, { capKiB: budgets.firstLoadKb, growthPct: budgets.firstLoadGrowthPct, baseline: baseline?.firstLoad ?? null });
|
|
99
116
|
const largest = routes.reduce((a, b) => (b.bytes > a.bytes ? b : a), routes[0]);
|
|
100
117
|
const lines: string[] = [];
|
|
101
118
|
const firstStatus: Status = size.problems.length ? 'FAIL' : size.growth === 'not-configured' ? 'warn' : 'ok';
|
|
@@ -104,23 +121,30 @@ async function main(): Promise<number> {
|
|
|
104
121
|
for (const p of size.problems) lines.push(`FAIL ${p}`);
|
|
105
122
|
|
|
106
123
|
const capped = Object.keys(budgets.htmlKb);
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
124
|
+
// A page the build no longer has (a created project leaves the Atlas out) is not measured: there is nothing to hold.
|
|
125
|
+
const built = new Set(routes.map((r) => r.route));
|
|
126
|
+
const held = Object.keys(baseline?.html ?? {}).filter((r) => built.has(r));
|
|
127
|
+
const pages = [...new Set([...held, ...capped])].sort();
|
|
128
|
+
let html: { status: Status; sizes: Record<string, number>; growth?: 'checked' | 'not-configured'; reason?: string; problems: string[] };
|
|
129
|
+
if (!base) html = { status: 'not run', sizes: {}, reason: 'no app is running to measure', problems: [] };
|
|
110
130
|
else {
|
|
111
|
-
const got = await htmlSizes(
|
|
112
|
-
const
|
|
113
|
-
|
|
131
|
+
const got = await htmlSizes(pages, 'its HTML is held');
|
|
132
|
+
const grown = htmlGrowthProblems(got.sizes, { growthPct: budgets.htmlGrowthPct, baseline: baseline?.html ?? null });
|
|
133
|
+
const problems = [...got.problems, ...grown.problems, ...htmlProblems(got.sizes, budgets.htmlKb)];
|
|
134
|
+
html = { status: problems.length ? 'FAIL' : grown.growth === 'not-configured' ? 'warn' : 'ok', sizes: got.sizes, growth: grown.growth, problems };
|
|
114
135
|
}
|
|
136
|
+
const largestPage = Object.entries(html.sizes).sort((a, b) => b[1] - a[1])[0];
|
|
115
137
|
lines.push(html.status === 'not run'
|
|
116
138
|
? `not run html ${html.reason}`
|
|
117
|
-
: `${html.status === 'FAIL' ? 'FAIL ' : 'ok '} html ${
|
|
139
|
+
: `${html.status === 'FAIL' ? 'FAIL ' : html.status === 'warn' ? 'warn ' : 'ok '} html ${Object.keys(html.sizes).length} pages gzipped${largestPage ? `, largest ${largestPage[0]} ${(largestPage[1] / KIB).toFixed(1)} KiB` : ''}`
|
|
140
|
+
+ ` · growth ${html.growth === 'checked' ? `within ${budgets.htmlGrowthPct}% of the baseline` : 'not-configured (no html in scripts/verify.baseline.json: node scripts/sizes.ts --write-baseline --base <url>)'}`
|
|
141
|
+
+ (capped.length ? ` · caps ${capped.map((r) => `${r} ${html.sizes[r] === undefined ? 'unmeasured' : `${(html.sizes[r] / KIB).toFixed(1)} KiB`} of ${budgets.htmlKb[r]}`).join(', ')}` : ''));
|
|
118
142
|
for (const p of html.problems) lines.push(`FAIL ${p}`);
|
|
119
143
|
lines.forEach((l) => console.log(l));
|
|
120
144
|
|
|
121
145
|
const failed = firstStatus === 'FAIL' || html.status === 'FAIL';
|
|
122
146
|
summary({
|
|
123
|
-
status: failed ? 'FAIL' : firstStatus === 'warn' ? 'warn' : 'ok',
|
|
147
|
+
status: failed ? 'FAIL' : firstStatus === 'warn' || html.status === 'warn' ? 'warn' : 'ok',
|
|
124
148
|
firstLoad: { status: firstStatus, routes: routes.length, largest: { route: largest.route, kib: kib(largest.bytes) }, growth: size.growth, problems: size.problems },
|
|
125
149
|
html,
|
|
126
150
|
});
|
|
@@ -30,5 +30,31 @@
|
|
|
30
30
|
"/system/system/[id]": 683852,
|
|
31
31
|
"/tests": 804568,
|
|
32
32
|
"/usage": 809012
|
|
33
|
+
},
|
|
34
|
+
"html": {
|
|
35
|
+
"method": "the complete streamed response, gzipped (zlib level 6), bytes",
|
|
36
|
+
"routes": {
|
|
37
|
+
"/": 33515,
|
|
38
|
+
"/business": 41229,
|
|
39
|
+
"/customers": 71693,
|
|
40
|
+
"/embed/home": 18484,
|
|
41
|
+
"/embed/now": 16620,
|
|
42
|
+
"/embed/proposal": 6052,
|
|
43
|
+
"/embed/requests": 8142,
|
|
44
|
+
"/engineering": 35698,
|
|
45
|
+
"/keys": 16088,
|
|
46
|
+
"/members": 22179,
|
|
47
|
+
"/operations-tickets": 21416,
|
|
48
|
+
"/releases": 19263,
|
|
49
|
+
"/requests": 22288,
|
|
50
|
+
"/services": 19814,
|
|
51
|
+
"/settings": 15697,
|
|
52
|
+
"/sign-in": 7864,
|
|
53
|
+
"/system": 32274,
|
|
54
|
+
"/system/states/error": 9816,
|
|
55
|
+
"/system/states/loading": 11576,
|
|
56
|
+
"/tests": 19773,
|
|
57
|
+
"/usage": 44731
|
|
58
|
+
}
|
|
33
59
|
}
|
|
34
60
|
}
|
|
@@ -99,12 +99,15 @@ type VerifyConfig = {
|
|
|
99
99
|
/**
|
|
100
100
|
* Limits verify enforces. Leave a key out for Meridian's default: first-load JS 820 KiB per route and 5% growth over
|
|
101
101
|
* `scripts/verify.baseline.json`, the warm, cold and after-live navigation times, and no prefetch on a closed phone drawer.
|
|
102
|
-
*
|
|
102
|
+
* Every page's HTML, gzipped through the end of the streamed response, is held to `htmlGrowthPct` (5%) over its own
|
|
103
|
+
* figure in the baseline's `html`; `htmlKb` adds an absolute cap in KiB, the same gzipped figure, for a page a project
|
|
104
|
+
* wants pinned, and has no default.
|
|
103
105
|
*/
|
|
104
106
|
budgets?: {
|
|
105
107
|
navigation?: { warm?: ReadinessBudget; cold?: ReadinessBudget; afterLive?: ReadinessBudget };
|
|
106
108
|
firstLoadKb?: number;
|
|
107
109
|
firstLoadGrowthPct?: number;
|
|
110
|
+
htmlGrowthPct?: number;
|
|
108
111
|
htmlKb?: Record<string, number>;
|
|
109
112
|
prefetchKb?: { desktop: number; phoneClosed: number };
|
|
110
113
|
};
|
|
@@ -152,17 +155,6 @@ const config: VerifyConfig = {
|
|
|
152
155
|
smokeRoutes: ['/', '/requests', '/settings'],
|
|
153
156
|
// The sample's own journeys across its three workspaces, run beside the audit, the presses and the keyboard walk.
|
|
154
157
|
browserChecks: ['journeys/workspaces.ts'],
|
|
155
|
-
// Sizes to hold. /services is the longest streamed document of the analytical pages: six services each with their own
|
|
156
|
-
// ninety days of bars and the screen-reader summary behind each, an incident timeline and the resolved incidents. It
|
|
157
|
-
// measured 99-100 KiB when this cap was set, so the cap sat exactly on the measurement and the next legitimate
|
|
158
|
-
// byte failed: the access work that gave every page a gate added 0.6% (101 KiB, 103,025 bytes), which is markup
|
|
159
|
-
// the page needs rather than fat to trim. Then the page came to lead with a Featured metric of uptime, whose chart is
|
|
160
|
-
// the share of services operational on each of the ninety days: 106 KiB. Held at 110 — above the measurement, so the
|
|
161
|
-
// next few KiB still fail.
|
|
162
|
-
// /customers hands its client every customer, because the list filters, sorts and pages them in the browser as the
|
|
163
|
-
// request log does: 329 rows of the nine fields a row draws measured 318 KiB (325,841 bytes) when the sample became a
|
|
164
|
-
// company. Held at 330, so a field added to a row, or a second copy of the rows, fails here.
|
|
165
|
-
budgets: { htmlKb: { '/services': 110, '/requests': 150, '/customers': 330 } },
|
|
166
158
|
navigationChecks: [
|
|
167
159
|
{ path: '/', title: 'Home', readySelector: 'main table tbody tr', probe: 'toggle', controlSelector: 'main [role="radio"][aria-checked="false"]', resultSelector: 'main [role="radio"]' },
|
|
168
160
|
{ path: '/members', title: 'Members', readySelector: 'main table tbody tr', probe: 'toggle', controlSelector: 'main button[aria-haspopup="menu"]', resultSelector: '[role="menu"]' },
|
|
@@ -299,6 +299,8 @@ covered = { data: coverage.routes.filter((r) => r.data === 'configured').length,
|
|
|
299
299
|
const unsafe = unsafeBackend();
|
|
300
300
|
const chromeMissing = fs.existsSync(chromePath) ? null : `Chrome not found at ${chromePath} (set CHROME)`;
|
|
301
301
|
const baselinePath = 'scripts/verify.baseline.json';
|
|
302
|
+
/** Whether the baseline records each page's HTML; one that cannot be read is sizes.ts's to report, after the build. */
|
|
303
|
+
const baselineHasHtml = (file: string) => { try { return typeof JSON.parse(fs.readFileSync(file, 'utf8')).html === 'object'; } catch { return true; } };
|
|
302
304
|
|
|
303
305
|
if (wide || perf) {
|
|
304
306
|
// A deep mode needs all of it, and says every piece that is missing at once, before it spends a build.
|
|
@@ -307,7 +309,11 @@ if (wide || perf) {
|
|
|
307
309
|
...(railReason ? [railReason] : []),
|
|
308
310
|
...(unsafe ? [unsafe.fix] : []),
|
|
309
311
|
...(chromeMissing ? [chromeMissing] : []),
|
|
310
|
-
...(!full
|
|
312
|
+
...(!full ? [] : !fs.existsSync(path.join(ROOT, baselinePath))
|
|
313
|
+
? [`${baselinePath} is missing: record it with node scripts/sizes.ts --write-baseline --base <url> from a running build whose sizes you reviewed`]
|
|
314
|
+
: !baselineHasHtml(path.join(ROOT, baselinePath))
|
|
315
|
+
? [`${baselinePath} holds no HTML: record it with node scripts/sizes.ts --write-baseline --base <url> from a running build whose sizes you reviewed`]
|
|
316
|
+
: []),
|
|
311
317
|
];
|
|
312
318
|
if (missing.length) {
|
|
313
319
|
for (const m of missing) log(`FAIL ${m}`);
|
|
@@ -465,7 +471,8 @@ const sizes = await run('scripts/sizes.ts', baseUrl ? ['--base', baseUrl] : [],
|
|
|
465
471
|
if (m) phase(m[1] as Status, m[2], sizes.took, m[3]); else beneath(l.replace(/^FAIL\s+/, ''));
|
|
466
472
|
}
|
|
467
473
|
const html = summaryOf<{ html: { status: Status } }>(sizes.out, 'sizes')?.html.status;
|
|
468
|
-
|
|
474
|
+
// Every page's HTML is held to its baseline, so HTML that was not measured is a check that did not run.
|
|
475
|
+
htmlMissing = html === 'not run';
|
|
469
476
|
}
|
|
470
477
|
|
|
471
478
|
// 5. The navigation smoke of the selected routes; every rail route in --full and --timed.
|
|
@@ -140,8 +140,8 @@ in `optional:src/views/settings.tsx`.
|
|
|
140
140
|
|
|
141
141
|
Three things an adopter meets here. The layout's promise is a request-time part of every page, so each page's HTML
|
|
142
142
|
carries that part's flight data beside the prerendered one: one adopter measured a page going from 65 to 90 KiB of
|
|
143
|
-
HTML (11.2 to 12.0 KiB compressed), so a team
|
|
144
|
-
|
|
143
|
+
HTML (11.2 to 12.0 KiB compressed), so a team should expect each page's HTML
|
|
144
|
+
figure to move and record its baseline again (`node scripts/sizes.ts --write-baseline --base <url>`). The assistant walk-through in `pnpm verify --full` drives the sample's own Home,
|
|
145
145
|
Now, Members (its lookups through `explore_members`, each request handing it), API keys (a key's revocation, found
|
|
146
146
|
through `explore_keys`, expiring on moving on) and Settings, asks the sample's Usage and Tests through their prepared
|
|
147
147
|
queries, and reports those steps as not applicable in a product that replaced them; what the product's own
|
|
@@ -5,7 +5,7 @@ team's. This list is the rule the classifier itself uses; edit it in `cli/src/ow
|
|
|
5
5
|
|
|
6
6
|
<!-- BEGIN:ownership -->
|
|
7
7
|
- Managed: the payload files under `tokens/`, `src/styles/` and `src/components/`, except each component's `README.md` and `preview.tsx`.
|
|
8
|
-
- Managed: the distributed scripts under `scripts/`, except `scripts/verify.config.ts`, the
|
|
8
|
+
- Managed: the distributed scripts under `scripts/`, except `scripts/verify.config.ts`, the size baseline `optional:scripts/verify.baseline.json` (first-load JS and the HTML of each page) and a team-local `optional:scripts/check.local.ts`.
|
|
9
9
|
- Managed: the library helpers `src/lib/cn.ts`, `src/lib/format.ts`, `src/lib/format-date.ts`, `src/lib/period.ts`, `src/lib/color.ts`, `src/lib/host.ts`, `src/lib/preferences.ts`, `src/lib/csv.ts`, `src/lib/safe-markdown.ts`, `src/lib/logo.ts`, `src/lib/collection.ts`, `src/lib/explore.ts`, `src/lib/live.ts`, `src/lib/shared-context.ts`, `src/lib/agent-guidance.ts`, `src/lib/insight.ts` and `src/lib/facts.ts`, and the assistant prompt `src/lib/assistant/prompt.ts`.
|
|
10
10
|
- Managed: `src/views/console-chrome.tsx` and `tests/setup.ts`.
|
|
11
11
|
- Managed: `optional:scripts/package.json`, only when the adopt or create that set the project up generated it for this project shape.
|
|
@@ -90,10 +90,11 @@ Each floor is measured by a command, and its evidence is the line the command pr
|
|
|
90
90
|
you change it (its comments describe every field, and the template's own entries show the shape): a `navigationChecks`
|
|
91
91
|
entry for every rail route (a ready selector and one harmless probe that works at 390px too, where a table is a card
|
|
92
92
|
list with no column headers to sort: a filter, a link or a dialog), `detailRoutes` with ids from your data for each
|
|
93
|
-
state worth seeing, `smokeRoutes` among the rail routes, `budgets.htmlKb
|
|
93
|
+
state worth seeing, `smokeRoutes` among the rail routes, `budgets.htmlKb`, when the project pins a page's HTML, naming only routes that exist, a `fakeApi` as
|
|
94
94
|
soon as a page calls an API, and in an adopted project a `fakeApi` or `noLiveApi` (a created project whose pages read
|
|
95
95
|
only its own sample needs neither). After the pages change, look at the sizes `pnpm verify` prints and record them with
|
|
96
|
-
`node scripts/sizes.ts --write-baseline
|
|
96
|
+
`node scripts/sizes.ts --write-baseline --base <url>` against the running build once you have judged them: first-load JS
|
|
97
|
+
for every route and the gzipped HTML of every page, each then held to 5% growth over its own figure. `--full` and `--timed` refuse, listing every gap,
|
|
97
98
|
until these hold.
|
|
98
99
|
|
|
99
100
|
### The craft: judged on the renders, with evidence
|
|
@@ -169,7 +169,7 @@ update (`2` pending, `0` finalized, `1` failure). `--abort`: `0` aborted, `1` re
|
|
|
169
169
|
|
|
170
170
|
## Resolving 0.9.0's migrations
|
|
171
171
|
|
|
172
|
-
Moving a project to 0.9.0 can report two more `migration:<id>` items, to 0.10.0 a third for an assistant kept from before, to 0.13.0 a fourth for a navigation item without a `needs`, to 0.15.0 a fifth for a page under the project's own route group, and to 0.16.0 five more, for a configuration that does not declare `refusal`, an export gone from a managed module, a view tool without its page or its prepared query, a view tool's page whose specification does not say what the agent does there, and the sample's pages under their old names; to 0.16.2 one more, for a test of the rail standing over a `ResizeObserver` that answers `observe` with nothing, to 0.16.3 three more, for the assistant route's local `onPage`, a file importing the journey from `scripts/navigate.ts` and a route or a read that cuts today at the UTC date,
|
|
172
|
+
Moving a project to 0.9.0 can report two more `migration:<id>` items, to 0.10.0 a third for an assistant kept from before, to 0.13.0 a fourth for a navigation item without a `needs`, to 0.15.0 a fifth for a page under the project's own route group, and to 0.16.0 five more, for a configuration that does not declare `refusal`, an export gone from a managed module, a view tool without its page or its prepared query, a view tool's page whose specification does not say what the agent does there, and the sample's pages under their old names; to 0.16.2 one more, for a test of the rail standing over a `ResizeObserver` that answers `observe` with nothing, to 0.16.3 three more, for the assistant route's local `onPage`, a file importing the journey from `scripts/navigate.ts` and a route or a read that cuts today at the UTC date, to 0.17.0 one more, for a file calling `finalOutcome` with a boolean, and to 0.19.0 one more, for a test that expects a Column chart's period with no data to draw a track. Each is resolved the same way as those below:
|
|
173
173
|
|
|
174
174
|
| Id | The old shape it found | The change |
|
|
175
175
|
|----|------------------------|------------|
|
|
@@ -188,6 +188,7 @@ Moving a project to 0.9.0 can report two more `migration:<id>` items, to 0.10.0
|
|
|
188
188
|
| `journey-moved` (0.16.3) | a file of the team's importing anything but `workspaceHomes` from `scripts/navigate.ts`, such as a copy of `optional:tests/navigate-start.test.ts` | the journey moved to `scripts/lib/journey.ts`: bring `optional:tests/navigate-start.test.ts` and `optional:tests/navigate-menu-start.test.ts` over from the release as it has them, and import the journey from `scripts/lib/journey.ts` in a file of your own; checked by the gate |
|
|
189
189
|
| `today-in-timezone` (0.16.3) | `optional:app/api/assistant/route.ts` or `optional:src/data/metrics.ts` cutting today with `clock().toISOString().slice(0, 10)` | `dayOf(clock())` from `src/lib/format-date.ts`, so today is the day in `app.timezone`; a file of your own comparing an instant's day with a day row asks `dayOf` too; checked by the gate |
|
|
190
190
|
| `verify-tiers` (0.17.0) | a file of the team's importing `finalOutcome` from `scripts/lib/coverage.ts` and calling it with a boolean, such as a copy of `optional:tests/verify-suite-outcome.test.ts` from before the tiers | `finalOutcome` takes `{ full, timed }` since verify has a timed tier: bring `optional:tests/verify-suite-outcome.test.ts` over from the release as it has it, and call `finalOutcome({ full, timed }, notRun)` in a file of your own, as `validation.md` describes; checked by the gate |
|
|
191
|
+
| `no-data-mark` (0.19.0) | a test of the team's importing the Column chart and expecting a `.bg-fill-track` in a period with no data, such as `optional:tests/column-chart-no-data.test.tsx` as the template wrote it | a `null` period draws a dashed hairline on the baseline since 0.19.0, not a track the height of the scale, which was the tallest mark on the chart: bring `optional:tests/column-chart-no-data.test.tsx` over from the release as it has it, and find `[data-no-data-mark]` where a test of your own found `.bg-fill-track` or `[data-track]`; checked by the gate |
|
|
191
192
|
|
|
192
193
|
A created project that never changed the template's pages brings the release's version of each affected file over, with
|
|
193
194
|
the files that arrived with them: the view contexts `optional:src/views/home-context.ts` and its siblings, the view
|
|
@@ -217,7 +218,7 @@ each one in the project, then record it as above, with the hashes of the files a
|
|
|
217
218
|
| `live-provider` | collections without `optional:src/data/live-actions.ts`, or a console layout without the provider | add the live files: `live.md`, "The live starters" and "Refreshing" |
|
|
218
219
|
| `authorized-endpoints` | the assistant route or an actions file that never calls `resolveAccess` | resolve the caller first, and bring `optional:src/lib/assistant/tools.ts` and `optional:src/lib/assistant/respond.ts` over, which take the guard: "The assistant route" |
|
|
219
220
|
|
|
220
|
-
Every migration but `verify-modes` is checked by the gate and a build; `verify-modes` by the gate alone. After 0.5.0 the gate alone checks `agent-reads-section`, `nav-needs`, `agent-reads-own-group`, `agent-does-here-section`, `marker-measured-on-observe`, `view-page-matched-as-next`, `journey-moved`, `today-in-timezone` and `
|
|
221
|
+
Every migration but `verify-modes` is checked by the gate and a build; `verify-modes` by the gate alone. After 0.5.0 the gate alone checks `agent-reads-section`, `nav-needs`, `agent-reads-own-group`, `agent-does-here-section`, `marker-measured-on-observe`, `view-page-matched-as-next`, `journey-moved`, `today-in-timezone`, `verify-tiers` and `no-data-mark`.
|
|
221
222
|
|
|
222
223
|
How to make the change depends on whose file it is:
|
|
223
224
|
|