zz-meridian 0.17.0 → 0.18.0

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.
Files changed (87) hide show
  1. package/dist/context.js +1 -1
  2. package/dist/migrations.js +9 -9
  3. package/package.json +1 -1
  4. package/payload/CHANGELOG.md +41 -0
  5. package/payload/CONTRIBUTING.md +2 -2
  6. package/payload/README.md +1 -1
  7. package/payload/app/(dashboard)/engineering/page.tsx +1 -1
  8. package/payload/app/(dashboard)/keys/page.tsx +2 -1
  9. package/payload/app/(dashboard)/members/page.tsx +2 -2
  10. package/payload/app/(dashboard)/operations-tickets/page.tsx +2 -1
  11. package/payload/app/(dashboard)/releases/page.tsx +2 -2
  12. package/payload/app/(dashboard)/tests/page.tsx +2 -2
  13. package/payload/app/api/live/route.ts +19 -1
  14. package/payload/docs/agents.md +2 -0
  15. package/payload/docs/benchmark.md +1 -1
  16. package/payload/docs/distribution.md +3 -3
  17. package/payload/docs/skill-evals.md +1 -1
  18. package/payload/docs/start-a-dashboard.md +1 -1
  19. package/payload/docs/testing.md +36 -27
  20. package/payload/journeys/workspaces.ts +5 -1
  21. package/payload/package.json +1 -1
  22. package/payload/scripts/audit.ts +7 -0
  23. package/payload/scripts/fake-llm.ts +22 -11
  24. package/payload/scripts/interactions.ts +9 -4
  25. package/payload/scripts/lib/chrome.ts +5 -0
  26. package/payload/scripts/lib/config-hooks.ts +17 -0
  27. package/payload/scripts/lib/coverage.ts +2 -2
  28. package/payload/scripts/lib/frame-cause.ts +41 -0
  29. package/payload/scripts/lib/journey.ts +11 -5
  30. package/payload/scripts/lib/links.ts +14 -0
  31. package/payload/scripts/lib/phone.ts +54 -0
  32. package/payload/scripts/lib/shard.ts +23 -0
  33. package/payload/scripts/lib/sizes.ts +8 -2
  34. package/payload/scripts/live.ts +6 -6
  35. package/payload/scripts/navigate.ts +1 -1
  36. package/payload/scripts/perf.ts +15 -4
  37. package/payload/scripts/shot.ts +22 -4
  38. package/payload/scripts/verify.baseline.json +29 -29
  39. package/payload/scripts/verify.config.ts +14 -6
  40. package/payload/scripts/verify.ts +52 -7
  41. package/payload/scripts/vitals.ts +32 -21
  42. package/payload/skills/zz-meridian/references/cache.md +13 -8
  43. package/payload/skills/zz-meridian/references/existing-project.md +5 -1
  44. package/payload/skills/zz-meridian/references/live.md +52 -8
  45. package/payload/skills/zz-meridian/references/standard.md +1 -1
  46. package/payload/skills/zz-meridian/references/update.md +2 -2
  47. package/payload/skills/zz-meridian/references/validation.md +22 -16
  48. package/payload/src/app.config.ts +6 -2
  49. package/payload/src/components/base/shell/README.md +1 -1
  50. package/payload/src/components/base/shell/index.tsx +25 -12
  51. package/payload/src/components/base/use-nav-drawer.ts +31 -3
  52. package/payload/src/components/patterns/metric-tile/README.md +1 -1
  53. package/payload/src/components/patterns/metric-tile/index.tsx +2 -1
  54. package/payload/src/components/patterns/rail/README.md +2 -2
  55. package/payload/src/components/patterns/rail/index.tsx +6 -0
  56. package/payload/src/components/ui/segmented/README.md +2 -0
  57. package/payload/src/components/ui/segmented/index.tsx +8 -7
  58. package/payload/src/data/README.md +1 -1
  59. package/payload/src/data/engineering.ts +10 -5
  60. package/payload/src/data/live-stream.ts +12 -4
  61. package/payload/src/data/metrics.ts +38 -1
  62. package/payload/src/data/read.ts +13 -8
  63. package/payload/src/data/tickets.ts +4 -4
  64. package/payload/src/lib/collection.ts +18 -3
  65. package/payload/src/lib/live.ts +90 -22
  66. package/payload/src/styles/base.css +12 -0
  67. package/payload/src/system/atlas-shell.tsx +7 -6
  68. package/payload/src/system/page-stage.tsx +7 -2
  69. package/payload/src/views/home.tsx +3 -3
  70. package/payload/src/views/keys.tsx +3 -3
  71. package/payload/src/views/members.tsx +3 -3
  72. package/payload/src/views/now.tsx +5 -5
  73. package/payload/src/views/releases.tsx +3 -3
  74. package/payload/src/views/revenue.tsx +3 -3
  75. package/payload/src/views/services-context.ts +10 -1
  76. package/payload/src/views/services.tsx +2 -3
  77. package/payload/src/views/tests.tsx +3 -3
  78. package/payload/src/views/tickets.tsx +4 -3
  79. package/payload/src/views/tools.ts +11 -11
  80. package/payload/tests/explore-runner.test.ts +6 -4
  81. package/payload/tests/live-client.test.tsx +78 -0
  82. package/payload/tests/live-resume.test.ts +65 -0
  83. package/payload/tests/live-revision-boot.test.ts +20 -0
  84. package/payload/tests/metric-tile-title.test.tsx +14 -0
  85. package/payload/tests/segmented-measure.test.tsx +40 -0
  86. package/payload/tests/shell-drawer-open.test.tsx +49 -0
  87. package/payload/tests/workspace-data-paths.test.ts +1 -1
package/dist/context.js CHANGED
@@ -20,7 +20,7 @@ When it is absent, use the skill's brief template with the person's answers; do
20
20
  - Before finishing: safely configured \`pnpm verify\` for the default gate/build/smoke path; report any partial coverage.
21
21
  Use \`pnpm gate\` alone for static feedback. Do not automatically chain it before verify, which already runs it.
22
22
  Use \`pnpm verify --full\` for the exhaustive checks and \`pnpm verify --timed\` for the timed ones (it needs a quiet
23
- machine); \`--full --timed\` is the whole standard. Weekly CI runs \`--full --timed --perf\`.
23
+ machine); \`--full --timed\` is the whole standard. The 20-sample \`--perf\` protocol runs only in CI, split by route with \`--shard <i>/<n>\`; locally use \`node scripts/perf.ts --routes <route>\`. Weekly CI runs each tier as its own job.
24
24
  - Agents in the product read through authorized collections and write only through an approved Proposal.
25
25
  ${END}`;
26
26
  /** The brief a team keeps at `docs/brief.md`; its guidance lines equal `BRIEF_GUIDANCE` in the assistant prompt. */
@@ -236,7 +236,7 @@ export const RELEASE_MIGRATIONS = [
236
236
  since: '0.5.0',
237
237
  applies: removedVerifyFlags,
238
238
  summary: 'verify has four modes, and --quick, --no-vitals and --extra are gone.',
239
- instructions: 'Read "Validation" in references/validation.md. Run `pnpm verify` for the bounded default (the gate once, one build, the size checks and a smoke of up to three routes), `pnpm verify --full` for every exhaustive suite, `pnpm verify --timed` for the timed ones (on a quiet machine) and `pnpm verify --perf` for the 20-sample navigation protocol, and replace the removed flags in the script, workflow or shell file. Read the coverage line each run ends with: it says what ran and what did not.',
239
+ instructions: 'Read "Validation" in references/validation.md. Run `pnpm verify` for the bounded default (the gate once, one build, the size checks and a smoke of up to three routes), `pnpm verify --full` for every exhaustive suite, `pnpm verify --timed` for the timed ones (on a quiet machine) and, in CI only, `pnpm verify --perf --shard <i>/<n>` for the 20-sample navigation protocol (`node scripts/perf.ts --routes <route>` narrows it to one route on your machine), and replace the removed flags in the script, workflow or shell file. Read the coverage line each run ends with: it says what ran and what did not.',
240
240
  checks: ['gate'],
241
241
  },
242
242
  {
@@ -411,7 +411,7 @@ export const RELEASE_MIGRATIONS = [
411
411
  since: '0.16.0',
412
412
  applies: viewToolPagesWithoutAgentDoes,
413
413
  summary: 'The specification of a page a view tool renders says what the agent does there.',
414
- instructions: 'Read "Prepared queries" in references/agents.md. Under `## Agents` in the README.md of each page a view tool\'s `page` names, add `### What the agent does here`: a line `Protagonist: <the card\'s name>.`, then at least three questions a person on the page asks that its context does not answer alone, each `- "<question>" — <what the prepared query is asked>`, and mark that card on the page with `data-protagonist="<name>"`. `scripts/check.ts` fails the gate on such a page without the part. While a view tool in `src/views/tools.ts` names no page yet (`view-tool-page-explore`), every console page specification without the part is listed: one that no tool comes to name needs nothing.',
414
+ instructions: 'Read "Prepared queries" in references/agents.md. Under `## Agents` in the README.md of each page a view tool\'s `page` names, add `### What the agent does here`: a line `Protagonist: <the card\'s name>.`, then at least three questions a person on the page asks that its context does not answer alone, each `- "<question>" — <what the prepared query is asked>`, and mark that card on the page with `data-protagonist="<name>"`. `scripts/check.ts` fails the gate on such a page without the part. While a view tool names no page yet (`view-tool-page-explore`), every console page specification without the part is listed: one that no tool comes to name needs nothing.',
415
415
  checks: ['gate'],
416
416
  },
417
417
  {
@@ -456,7 +456,7 @@ export const RELEASE_MIGRATIONS = [
456
456
  },
457
457
  {
458
458
  id: 'verify-tiers',
459
- since: '0.16.4',
459
+ since: '0.17.0',
460
460
  applies: (root) => matching(root, (rel, src) => importsOf(rel, src).some(({ from, names }) => from === 'scripts/lib/coverage' && names.includes('finalOutcome')) && /(?<![\w.$])finalOutcome\(\s*(?!\{)/.test(src)),
461
461
  summary: '`finalOutcome` in `scripts/lib/coverage.ts` takes `{ full, timed }` as its first argument, because verify has a timed tier apart from the full one.',
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`.',
@@ -480,15 +480,15 @@ function removedExportReaders(root) {
480
480
  }));
481
481
  }
482
482
  /**
483
- * The specifications of the pages the view tools of `src/views/tools.ts` render, as the gate reads them from 0.16.0,
484
- * that lack "What the agent does here". While a tool names no page, the gate cannot tell its page, and neither can this:
485
- * every console page specification without the part is listed then.
483
+ * The specifications of the pages the view tools render, as the gate reads them from 0.16.0, that lack "What the agent
484
+ * does here". The registry is `src/views/tools.ts`, and a tool may be defined in any module under `src/` it imports, as
485
+ * the gate reads them since 0.17.0, so every `src/` module is read. While a tool names no page, the gate cannot tell its
486
+ * page, and neither can this: every console page specification without the part is listed then.
486
487
  */
487
488
  function viewToolPagesWithoutAgentDoes(root) {
488
- const tools = text(root, 'src/views/tools.ts');
489
- if (tools === null)
489
+ if (text(root, 'src/views/tools.ts') === null)
490
490
  return null;
491
- const calls = viewToolCalls(tools).filter((k) => !k.has('...'));
491
+ const calls = teamSources(root).filter((f) => f.rel.startsWith('src/')).flatMap((f) => viewToolCalls(f.src)).filter((k) => !k.has('...'));
492
492
  if (calls.length === 0)
493
493
  return null;
494
494
  const pages = new Set(calls.map((k) => k.get('page')).filter((p) => typeof p === 'string'));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zz-meridian",
3
- "version": "0.17.0",
3
+ "version": "0.18.0",
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",
@@ -2,6 +2,47 @@
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.18.0] · 2026-10-19
6
+
7
+ ### Breaking
8
+
9
+ - **The phone the timed suites emulate is one device on every host: the device of Lighthouse benchmark index 800**, the top of Lighthouse's Mid-Tier Mobile bracket. `scripts/vitals.ts` and the navigation journey (`pnpm verify --timed` and `--perf`) measure the host's own index once per run (Lighthouse's `computeBenchmarkIndex`, the median of three readings) and slow the CPU by that index over 800, to one decimal and never below 1, where they slowed it four times on every host. A host whose index is above 3,200 is now slowed more than before and one below it less, so a laptop and a runner whose CPU varies between runs are held to the same phone. Each timed suite prints `phone: this host slowed <rate>x (benchmark index <index> …)`, and `pnpm verify` keeps that line under its navigation and Web Vitals phases. The floors and the budgets are unchanged. A near miss that used to pass on a fast machine is a page to speed up, not a budget to move.
10
+ - **The desktop prefetch cap counts every router prefetch.** It counted only requests carrying `next-router-prefetch: 1`, while Next 16 sends `3` for the runtime shell every link prefetches and `2` for a full prefetch's URL data, so it read a fraction of what the router fetched: Meridian's Home reads 45.3 KiB where it read 8.8 KiB. A project whose landing page now reads over its `budgets.prefetchKb.desktop` either prefetches less from that page or sets the cap from its own measurement.
11
+ - **The whole 20-sample protocol runs only in CI, split by route across runners.** `pnpm verify --perf` and `node scripts/perf.ts` without `--routes` now refuse where `CI` is not `true`, before the gate or Chrome starts: the protocol is 1,800 journeys, an hour and a half of a machine, and a person's machine is never quiet for that long. In CI, split it with `--shard <i>/<n>` (`pnpm verify --perf --shard 2/5`, with `--perf` alone; the shards together run every rail route once), one shard a runner. On your own machine, narrow it to the route in question: `node scripts/perf.ts --base <url> --routes <route>`. A script or agent prompt that ran `pnpm verify --perf` locally runs it in CI instead.
12
+
13
+ ### New
14
+
15
+ - **A project's own check that judges a time has a tier of its own** ([#48](https://github.com/zhixuan312/zz-meridian/issues/48)). `timedChecks` in `scripts/verify.config.ts` lists browser checks that judge a time: `pnpm verify --timed` runs each with `--base`, alone, after the live data and before Web Vitals, and reports them as the `timedChecks` suite. A check that measured a budget inside `browserChecks`, under the other suites' load, moves to `timedChecks`.
16
+ - **The fake model can call a project's own tools** ([#49](https://github.com/zhixuan312/zz-meridian/issues/49)). `fakeModel` in `scripts/verify.config.ts` lists `{ when, tool, input }` steps: for a question matching `when` (a case-insensitive pattern) the fake model calls `tool` with `input` when the request hands it, so a journey can check that the assistant cites what the page shows. A pattern that does not compile fails `pnpm verify` before the gate.
17
+ - **`scripts/shot.ts --as <persona>`** ([#50](https://github.com/zhixuan312/zz-meridian/issues/50)) signs each page in with that persona's cookie from `scripts/verify.config.ts` and names each file `<route>-<persona>-<width>-<theme>.png`, so a second persona's pages can be shot for a review. An undeclared persona is refused before Chrome starts, and any option of `shot.ts` given no value (`--as` last on the line) is refused rather than shot without it.
18
+
19
+ ### Changed
20
+
21
+ - **The start guide keeps the shared phone budget.** `/system/start/start-a-dashboard` carried its own phone-shell budget of 300 ms in `scripts/verify.config.ts`, against the shared 250, because its heading measured 243 to 282 ms on the phone. Since the rail's Docs item prefetches the page in full, its warm phone shell reads 28 ms on the machine that set the exception and 59 ms on the scheduled runner, so the exception is gone. A project's own `budget` on a navigation check works as before.
22
+ - **The fake model refuses the key only on the word `fail`.** It refused any message containing `fail`, so a question such as "Which runs failed?" could not be a `fakeModel` step; "Please fail now" is still refused.
23
+ - **Meridian's weekly CI runs each tier as its own job.** `full`, `timed`, `perf` (five shards of the rail routes) and `consumer` run in parallel, each on its own runner and each within half an hour, where one job ran `pnpm verify --full --timed --perf` for over two hours. A red is rerun by its job alone, and a dispatch can name the jobs it runs (`gh workflow run weekly.yml -f jobs=timed`). `docs/testing.md` says a project's own CI runs the perf protocol the same way.
24
+ - **A dropped frame says where its time went.** `scripts/vitals.ts`'s line for a page that drops a frame ends with the longest gap and, from the Long Animation Frames API, how much of it was script (and whose), style and layout, and render, or that no long animation frame covered it, which points at the compositor or the host rather than the page: `longest gap 167 ms: a long animation frame of 185 ms (script 181 ms by BUTTON.onclick, style and layout 0 ms, render 0 ms)`.
25
+
26
+ ### Fixed
27
+
28
+ - **`tests/explore-runner.test.ts` no longer fails on a busy machine.** Its large-collection case built a million rows to show that a prepared query asks one aggregate and reads no page, which took 2.8 s alone and passed the 30 s test timeout with the other test workers on a loaded host. It builds 120,000 now, and asserts the same calls. A created project keeps its own copy of the test: bring the release's over, or change `1_000_000` to `120_000` and `'750,010'` to `'90,000'`.
29
+ - **Opening the phone drawer and changing a period no longer lay the page out inside the tap.** Opening the navigation drawer focused its first link inside the tap and let the modal's changes to `<body>` (`pointer-events: none`, the scroll lock's scrollbar width) restyle the whole page, and a period change made the segmented control read its option's position inside the same task: each forced the browser to lay out the page before the frame, which dropped a frame of 67 to 118 ms on the scheduled runner's phone. The drawer stays a modal dialog: focus moves into it a frame after it opens and returns to the button that opened it, the shell's frame stops the inherited changes, and the segmented control measures its thumb in a `ResizeObserver` callback, after layout. A frame that holds a drawer (`data-drawer-frame`) now owns its `pointer-events` while the drawer is open. On the scheduled runner, whose style and layout run about twice as slow as its benchmark index says, some pages still drop a frame: the drawer's mount script and the period's re-render are the work left.
30
+ - **A live page no longer reads its collections again right after it loads** ([#53](https://github.com/zhixuan312/zz-meridian/issues/53)). The stream's first open re-read every collection the server had just served, so a page reading one path from two components and the stream asked for it three times a load, and under a slow API those reads held the browser's connections while the next navigation waited. A collection now has an optional `revision()`; `read`, `readAll` and `aggregate` return the revision they read at, a page hands its views those revisions, and the stream's first open sends them as `since`: the stream greets `synced` when nothing moved, and `resync` naming only what did, so no write between the server's read and the stream's subscription is lost. An adapter without `revision()` is resynced on every open, as before.
31
+ - **The standard Metric tile keeps a long label in its title** ([#51](https://github.com/zhixuan312/zz-meridian/issues/51)), as the compact tile does, so the audit no longer reports a cut label through this managed file.
32
+ - **A toggle probe reads the whole of what it watches** ([#52](https://github.com/zhixuan312/zz-meridian/issues/52)). The navigation compared the first 80 characters of each element its `resultSelector` matches, so a control that changes the end of a long row (an inline confirm in its last cell) was reported dead. A `resultSelector` that holds a ticking clock now counts as changing, so point it at what the control changes.
33
+ - **The console rail's Docs link finds the start guide ready.** The rail's Docs item asks for its page in full (`NavItem.prefetch`, a new optional field), since the start guide's article is its page's URL data, which the default App Shell prefetch leaves for after the press: from `/settings` the start guide's desktop shell fell from 77-146 ms to about 30 ms, and on the phone from 262 to 24 ms, at the cost of one prefetch of about 15 KiB on each main-workspace page on the desktop. The rail keeps Next's default for every other item, and compiles in a project whose `NavItem` has no such field. `scripts/live.ts` tells a prefetch from the router's own read by the same rule as the cap.
34
+ - **`node scripts/audit.ts --atlas` no longer reports overlaps on the Atlas pattern pages** (the 0.17.0 known issue). The audit measured `content-visibility: auto` subtrees the browser had not laid out yet; it now renders each such subtree before it measures and waits two frames. A `content-visibility: hidden` subtree is left alone, since it is not drawn.
35
+ - **The presses follow a relative link as a browser does.** `scripts/interactions.ts` skipped every `href` that did not start with `/`, so a relative link that answered 404 passed. It now resolves each link against its page (`linkPath` in `scripts/lib/links.ts`), follows every link to the page's own site, and reports `link <href> (<resolved path>) answers <status>`.
36
+ - **The Services page's uptime strip ends on today in `app.timezone`.** Its days were cut at the UTC date, so east of UTC after local midnight (west of UTC in the evening) the line's last day was a day off from the periods every other part of the page cuts with `dayOf`. A product whose `app.timezone` is UTC sees no change.
37
+ - **The checks no longer fill your Downloads folder.** Every press of an Export button (Home, Revenue and Usage) is a real download, and the browser `scripts/lib/chrome.ts` launches had no download directory of its own, so each `pnpm verify --full` and `--as` saved their CSVs in the person's Downloads folder, outside the temporary profile the run removes. Downloads now go inside that profile and are removed with it. Files an earlier run left there are named `home-<period>.csv`, `revenue-<period>.csv` and `usage-<period>.csv`, with ` (1)`, ` (2)` and so on after them, and can be deleted.
38
+ - **The workspace journeys read a page's heading once the page is there.** `journeys/workspaces.ts` read the heading as soon as the address and the rail were right, which on a loaded machine could be while the page's `loading.tsx` still stood in for it, whose masthead holds the page's name where the record's will be: the customer step failed with `found a page headed "Customer:"`. It now waits while a busy `role="status"` is on the page. A project that kept the sample's journeys brings `journeys/workspaces.ts` over from the release.
39
+ - **The `agent-does-here-section` update item finds a view tool kept in a module of its own.** It read `src/views/tools.ts` alone, so a project updating across 0.16.0 whose tools sat in modules the registry imports was told of no page while the gate, which reads those modules since 0.17.0, failed them. It now reads every module under `src/`.
40
+ - **A page specification's Theme control no longer drops a frame on a phone** (the 0.17.0 known issue). The stage keyed each pane by its theme, so a Dark-to-Light tap unmounted the pane and loaded the whole page again in a new frame inside the tap. Each pane is now keyed by its position: the frame on screen stays mounted and is re-dressed.
41
+
42
+ ### Known issues
43
+
44
+ - **On Meridian's scheduled runner, `pnpm verify --timed` drops one frame on two pages**: opening `/business`'s phone drawer and changing `/`'s period, in style and layout of 49 and 64 ms that the benchmark index does not calibrate (2 of 30 pages). Both pass on a quiet machine at the standard's phone, and no floor moved for them, so the weekly `timed` job stays red until the pages do less work in the tap. `pnpm verify` and `--full` are not affected.
45
+
5
46
  ## [0.17.0] · 2026-10-18
6
47
 
7
48
  ### Breaking
@@ -102,7 +102,7 @@ Write the status on the line under the title: `Status: beta`.
102
102
  | `pnpm verify` | The gate once, one production build, the route policy, the size checks and a smoke of up to three routes on desktop and the phone drawer; ends with a coverage line, and the report is in `out/verify.txt` | Before finishing any change |
103
103
  | `pnpm verify --full` | The navigation of every rail route, then every exhaustive suite: the audit, every control pressed and link followed, the keyboard walk, the assistant and the product's own browser journeys; it judges no time | After a change to shared components, the shell or data, and before a change is handed over |
104
104
  | `pnpm verify --timed` | The navigation of every rail route with its budgets judged, then the live data and Web Vitals, alone and last. It needs a quiet machine. `pnpm verify --full --timed` is the whole standard, the only run that can end "the project meets the Meridian standard" | After a change that could move a time; with `--full`, before a release of the product, at the release commit |
105
- | `pnpm verify --perf` | The 20-sample navigation protocol; combine with `--full` and `--timed` to run all three without repeating the gate or the build | When a navigation budget is in question, on a quiet machine; weekly in Meridian's repository |
105
+ | `pnpm verify --perf` | The 20-sample navigation protocol, only in CI, split by route with `--shard <i>/<n>` | Weekly in Meridian's repository (5 shards); narrowed locally, `node scripts/perf.ts --routes <route>`, to diagnose a route |
106
106
  | `node scripts/brand.ts --name … [--hue … --chroma …]` | Renames and rebrands a copy in place; a brand hue becomes an accent preset that holds contrast in every theme | Starting a project |
107
107
  | `node scripts/shot.ts <routes> --width 1440,390 --theme light,dark [--full]` | Screenshots into `out/shots/` | While editing |
108
108
 
@@ -117,7 +117,7 @@ A check is code, and it is wrong in the ways code is. Before a new check, or a c
117
117
 
118
118
  1. Add an entry to `CHANGELOG.md` under a new version. 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. Say in one line what breaks and what to do instead.
119
119
  2. `node cli/scripts/set-version.ts <v>`, then `pnpm gate` until it passes. Nothing else runs locally: the release workflow's gates are the proof. A release that changes the checks themselves (a check script, a tier, a budget) first meets the `Releases` rule in `docs/testing.md`.
120
- 3. Commit and push to `master`, then dispatch once: `gh workflow run release.yml -f version=<v>`. The workflow gates on the package's own tests (`tests/cli-*`) and the browser-fixture tests of the checks (`tests/deep-*`), one default `verify` within its time limit (whose gate leaves both groups out) and the consumer smoke from the tarball, publishes with provenance, checks the registry's tarball is the tested one, and tags last. `verify --full --timed --perf` runs weekly (`.github/workflows/weekly.yml`), not here.
120
+ 3. Commit and push to `master`, then dispatch once: `gh workflow run release.yml -f version=<v>`. The workflow gates on the package's own tests (`tests/cli-*`) and the browser-fixture tests of the checks (`tests/deep-*`), one default `verify` within its time limit (whose gate leaves both groups out) and the consumer smoke from the tarball, publishes with provenance, checks the registry's tarball is the tested one, and tags last. `verify --full`, `verify --timed` and `verify --perf` run weekly, each as its own job (`.github/workflows/weekly.yml`), not here.
121
121
 
122
122
  The `/release` command (`.claude/commands/release.md`) is this procedure as a runbook.
123
123
 
package/payload/README.md CHANGED
@@ -78,7 +78,7 @@ Dark is the default, written on `:root`; the light theme follows the operating s
78
78
  | `skills/zz-meridian/` | The agent skill (Claude Code, Codex) that builds dashboards on this template or brings it into yours, to the standard in `references/standard.md`; how it is evaluated is `docs/skill-evals.md` |
79
79
  | `cli/` | The `zz-meridian` npm package: `create`, `adopt`, `update`, `brand` and `skill`, its build, its smoke test and the fixture apps (`docs/distribution.md`) |
80
80
  | `.github/workflows/release.yml` | The release, each check once: the default verify within its time limit and the consumer path from the tarball (the release before updated; the last three weekly), then npm with provenance, the registry's bytes and provenance checked, then the tag (`.claude/commands/release.md`) |
81
- | `.github/workflows/weekly.yml` | Weekly, never at release: `verify --full --timed --perf` (perf a report) and the consumer smoke's recovery and failure cases; nothing waits on it |
81
+ | `.github/workflows/weekly.yml` | Weekly, never at release: one job per tier, `verify --full`, `verify --timed` and `verify --perf` in 5 shards (perf a report) and the consumer smoke's recovery and failure cases; nothing waits on it |
82
82
  | `Dockerfile`, `captain-definition` | The demo deployment: the template and the Atlas behind `DEMO_PASSWORD` (`proxy.ts`, `app/sign-in/README.md`), deployed to CapRover from this repository; never in the package |
83
83
 
84
84
  ## Get a dashboard on Meridian, in one sentence
@@ -60,5 +60,5 @@ async function Lead() {
60
60
  /** The parts, their strips packed one character a day for the trip to the browser. */
61
61
  async function Body() {
62
62
  const { data } = await readNow();
63
- return <PackedNowBody {...packNow(data)} />;
63
+ return <PackedNowBody {...packNow(data)} revisions={data.revisions} />;
64
64
  }
@@ -22,13 +22,14 @@ export function generateMetadata() {
22
22
  export default async function KeysPage() {
23
23
  const denied = await gate('keys');
24
24
  if (denied) return denied;
25
- const { rows, observedAt } = await readAll('keys');
25
+ const { rows, revision, observedAt } = await readAll('keys');
26
26
  const mayCreate = await may(ACTIONS['create-key'].needs);
27
27
  const mayRevoke = await may(ACTIONS['revoke-key'].needs);
28
28
  return (
29
29
  <KeysView
30
30
  rows={rows as ApiKey[]}
31
31
  now={observedAt}
32
+ revisions={revision ? { keys: revision } : undefined}
32
33
  may={{ create: mayCreate, revoke: mayRevoke }}
33
34
  createKey={createKey}
34
35
  revokeKey={revokeKey}
@@ -34,12 +34,12 @@ async function workspaceName(): Promise<string | undefined> {
34
34
  export default async function MembersPage() {
35
35
  const denied = await gate('members');
36
36
  if (denied) return denied;
37
- const { rows, observedAt } = await readAll('members', { sort: { field: 'joined', dir: 'desc' } });
37
+ const { rows, revision, observedAt } = await readAll('members', { sort: { field: 'joined', dir: 'desc' } });
38
38
  const members = rows as Member[];
39
39
  const [access, roles, workspace] = await Promise.all([
40
40
  Promise.all(members.map(async (m): Promise<readonly [string, MemberRowAccess]> => [m.id, await memberAccess(m.id)])).then((entries) => Object.fromEntries(entries)),
41
41
  invitableRoles(),
42
42
  workspaceName(),
43
43
  ]);
44
- return <MembersView rows={members} now={observedAt} actions={{ invite: inviteMember, setStatus: setMemberStatus, remove: removeMember, assignRole }} access={access} roles={roles} workspace={workspace} />;
44
+ return <MembersView rows={members} now={observedAt} revisions={revision ? { members: revision } : undefined} actions={{ invite: inviteMember, setStatus: setMemberStatus, remove: removeMember, assignRole }} access={access} roles={roles} workspace={workspace} />;
45
45
  }
@@ -22,7 +22,7 @@ export function generateMetadata() {
22
22
  export default async function TicketsPage() {
23
23
  const denied = await gate('tickets');
24
24
  if (denied) return denied;
25
- const [{ rows, observedAt }, mayCreate, mayUpdate, mayReadMembers, services] = await Promise.all([
25
+ const [{ rows, revisions, observedAt }, mayCreate, mayUpdate, mayReadMembers, services] = await Promise.all([
26
26
  readTickets(),
27
27
  may(ACTIONS['create-ticket'].needs),
28
28
  may(ACTIONS['update-ticket'].needs),
@@ -34,6 +34,7 @@ export default async function TicketsPage() {
34
34
  <TicketsView
35
35
  rows={rows}
36
36
  now={observedAt}
37
+ revisions={revisions}
37
38
  may={{ create: mayCreate, update: mayUpdate }}
38
39
  services={services.rows.map((s) => String(s.name))}
39
40
  assignees={people.map((m) => ({ id: String(m.id), name: String(m.name) }))}
@@ -40,6 +40,6 @@ export default async function ReleasesPage() {
40
40
  }
41
41
 
42
42
  async function ReleaseList() {
43
- const { rows, observedAt } = await readReleases();
44
- return <ReleasesView rows={rows} now={observedAt} />;
43
+ const { rows, revisions, observedAt } = await readReleases();
44
+ return <ReleasesView rows={rows} now={observedAt} revisions={revisions} />;
45
45
  }
@@ -40,6 +40,6 @@ export default async function TestsPage() {
40
40
  }
41
41
 
42
42
  async function Runs() {
43
- const { rows, observedAt } = await readRuns();
44
- return <TestsView rows={rows} now={observedAt} />;
43
+ const { rows, revisions, observedAt } = await readRuns();
44
+ return <TestsView rows={rows} now={observedAt} revisions={revisions} />;
45
45
  }
@@ -6,7 +6,21 @@ const NAME = /^[A-Za-z0-9_-]{1,64}$/;
6
6
 
7
7
  const text = (status: number, body: string) => new Response(body, { status, headers: { 'content-type': 'text/plain; charset=utf-8' } });
8
8
 
9
- /** `GET /api/live?collections=members,keys`: change hints for the collections the caller may read. 400 malformed, 401 without a session, 403 for any name they may not read. */
9
+ /** `name:revision,...` into a map; `undefined` when absent, `null` when a pair has no `:`, an empty part or an unnamed collection. */
10
+ function parseSince(raw: string | null, names: string[]): Record<string, string> | undefined | null {
11
+ if (raw === null) return undefined;
12
+ const served: Record<string, string> = {};
13
+ for (const pair of raw.split(',')) {
14
+ const at = pair.indexOf(':');
15
+ const name = pair.slice(0, at);
16
+ const revision = pair.slice(at + 1);
17
+ if (at < 1 || !revision || !names.includes(name)) return null;
18
+ served[name] = revision;
19
+ }
20
+ return served;
21
+ }
22
+
23
+ /** `GET /api/live?collections=members,keys`: change hints for the collections the caller may read; `since=members:<revision>` makes the first frame name only what moved. 400 malformed, 401 without a session, 403 for any name they may not read. */
10
24
  export async function GET(request: Request): Promise<Response> {
11
25
  const scope = await resolveAccess().catch((e: unknown) => { if (e instanceof Unauthenticated) return null; throw e; });
12
26
  if (!scope) return text(401, 'Sign in to continue.');
@@ -17,12 +31,16 @@ export async function GET(request: Request): Promise<Response> {
17
31
  const names = [...new Set(parts)];
18
32
  if (names.length > MAX_NAMES) return text(400, 'Name the collections to follow.');
19
33
 
34
+ const served = parseSince(new URL(request.url).searchParams.get('since'), names);
35
+ if (served === null) return text(400, 'Name the revisions as name:revision.');
36
+
20
37
  // An unknown name and a forbidden one answer alike, so neither is disclosed.
21
38
  if (!(await Promise.all(names.map((n) => can(scope, n, 'read')))).every(Boolean)) return text(403, 'You do not have access to that.');
22
39
 
23
40
  return liveStream({
24
41
  names,
25
42
  collections: names.map((n) => collectionFor(scope, n)),
43
+ since: served,
26
44
  signal: request.signal,
27
45
  authorize: async (name) => {
28
46
  const now = await resolveAccess().catch(() => null);
@@ -96,6 +96,8 @@ The page text it reads is the scroll region's visible text, less every node mark
96
96
 
97
97
  A product that replaced one of those pages gets `n/a` with the reason for its steps, decided from its files (`scripts/lib/sample.ts`), and every failure names the part it waited on.
98
98
 
99
+ A product checks what the assistant cites with a journey of its own (a script in `browserChecks` in `scripts/verify.config.ts`). To make the fake model call one of the product's view tools for a question, list it in `fakeModel` in the same file: `{ when, tool, input }`, where `when` is a case-insensitive regular expression's source. verify hands the steps to the fake model, which matches them before its own branches, calls `tool` with `input` where the request hands that tool (`I was not handed <tool> here.` where it does not), and answers with the tool's result. A question containing the word "fail" is still refused as a bad key, so a step's `when` should not need it alone.
100
+
99
101
  ## Building an MCP server for the template
100
102
 
101
103
  The template ships the views and their tool contracts; the server is yours. Every view's contract is in `src/views/tools.ts`: its `name`, the question it answers (`description`), the console route of its page (`page`), its address as a zod `input`, its `resourceUri` when it has an embed route, one `read` that returns what the view renders (`data`) and what both agents are told (`context`), and its prepared query (`explore`), which every view declares. The console's assistant offers each one as `view_<name>`, but registers it only where the caller satisfies that view's need (`may(v.needs)` in `app/api/assistant/route.ts`); register the same list, filtered the same way:
@@ -110,7 +110,7 @@ Those figures were measured before the suites looked inside open shadow roots. A
110
110
  - **Patterns:** a column chart left every other category unnamed; the assistant showed nothing between Send and its first words; narrow tiles read "vs previou…"; a read-only form drew its values at 1.5:1; three specimens contradicted their own captions.
111
111
  - **Pages:** a phone's record card left separator dots alone on a line and cut dates; Members looked unsorted because the column that explains its order was hidden; the Atlas home numbered its sections 01 to 05, which Meridian's own register rules out.
112
112
  - **Extreme values:** fed the largest figures the formatters print, a row of four tiles cut "9,876,543,210" to "9,876,54", a different number with no ellipsis, which no audit flags because the card clips it. A figure past nine characters now reads at a glance ("9.9B", "$1.2M"); the Extreme values specimens hold it. A probe that rewrote every name and figure in tables, lists and records to its longest also found nothing pushed sideways on any route.
113
- - **Measured:** `verify --full` on the final commit, when it still ran the live data and Web Vitals (both are `verify --timed`'s now), passes every suite: 0 audit issues on 22 routes at five widths in both themes, 361 controls and 56 links pressed, the keyboard walk, live data, and 18 of 18 pages under LCP 2.5 s, INP 200 ms and CLS 0.1. Web Vitals depend on a quiet machine: with other work holding the load at 11 to 38 on ten cores the same build read 10 to 17 of 18, because the four-times CPU slowdown applies to a host that is already busy; an A/B of the one shell change (the top bar as a container) showed INP tracking the load, not the build. Under reduced motion nothing moves on any route at 1440 or 390px.
113
+ - **Measured:** `verify --full` on the final commit, when it still ran the live data and Web Vitals (both are `verify --timed`'s now), passes every suite: 0 audit issues on 22 routes at five widths in both themes, 361 controls and 56 links pressed, the keyboard walk, live data, and 18 of 18 pages under LCP 2.5 s, INP 200 ms and CLS 0.1. Web Vitals depend on a quiet machine: with other work holding the load at 11 to 38 on ten cores the same build read 10 to 17 of 18, because the phone's CPU slowdown applies to a host that is already busy; an A/B of the one shell change (the top bar as a container) showed INP tracking the load, not the build. Under reduced motion nothing moves on any route at 1440 or 390px.
114
114
 
115
115
  ## Open
116
116
 
@@ -150,7 +150,7 @@ null. It only reads, and only team-owned files (the ownership rule's managed pat
150
150
  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 `verify-tiers`, `gate` alone), and its
151
151
  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.
152
152
 
153
- Fifteen 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), and `verify-tiers` (0.16.4). The ten of 0.5.0 are:
153
+ Fifteen 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), and `verify-tiers` (0.17.0). The ten of 0.5.0 are:
154
154
 
155
155
  | Id | Reported when a team-owned file still has | Completion step |
156
156
  |----|-------------------------------------------|-----------------|
@@ -247,8 +247,8 @@ Modelled on the release pipeline of the owner's earlier packages, one package in
247
247
  `dry_run` stops after step 4.
248
248
 
249
249
  **Weekly** (`.github/workflows/weekly.yml`, Mondays and on demand, master's current commit): the template's
250
- `verify --full --timed --perf`, one gate and one build for both (the perf part a report: a p95 over budget is a warning, a
251
- broken sample a failure), and the consumer smoke's `--adopt --update --origins 3 --update-all --verify` from a tarball packed from that
250
+ one job per tier on its own runner (`verify --full`, `verify --timed` and `verify --perf --shard <i>/5`, the perf part a report: a p95 over budget is a warning, a
251
+ broken sample a failure; a dispatch names its jobs with `-f jobs=timed`), and the consumer smoke's `--adopt --update --origins 3 --update-all --verify` from a tarball packed from that
252
252
  commit, labelled as the next patch so `update` treats it as unpublished: the adopted project's default verify cases,
253
253
  the update from each of the last three releases, the updater's stages on the 0.3.0 fixture, and every recovery and
254
254
  failure case of the update. Both run the
@@ -13,7 +13,7 @@ from it. Rebuilding a whole dashboard to judge a change in wording is not needed
13
13
  |---|---|---|---|
14
14
  | L0 static | Every path, command and flag the skill names exists (`node scripts/check.ts`); the text has no dated or contradictory instruction (a prompt audit, `/claude-api prompt-audit`) | seconds | every change to the skill |
15
15
  | L1 decisions | An agent reads the skill in a scenario's folder and stops before the first command that writes: its route, its announcement, its decisions, its brief, the views it will score, its definition of done and how it will evaluate | 2 to 5 minutes each, run side by side | a change to the route, the decisions, the brief or the standard |
16
- | L2 planted defects | An agent runs the loop on a built Meridian project with known defects planted (a clipped name, a missing empty state, a literal colour, a wrong total, a page with no protagonist, a stale `verify.config.ts` route): which it finds, which it fixes, and whether it stops only when `--full` prints the outcome | about 15 minutes | a change to the standard or the loop |
16
+ | L2 planted defects | An agent runs the loop on a built Meridian project with known defects planted (a clipped name, a missing empty state, a literal colour, a wrong total, a page with no protagonist, a stale `verify.config.ts` route): which it finds, which it fixes, and whether it stops only when `--full --timed` prints "the project meets the Meridian standard" | about 15 minutes | a change to the standard or the loop |
17
17
  | L3 end to end | The sentence, a fresh agent and a real request, from nothing (greenfield) or an existing app (brownfield) to the hand-over | 1 to 2 hours each | weekly, never per change |
18
18
 
19
19
  ## L1 scenarios
@@ -95,4 +95,4 @@ pnpm verify --full # every route, every control pressed and link followed, the
95
95
  pnpm verify --timed # the navigation budgets, the live data and Web Vitals; needs a quiet machine, runs alone and last
96
96
  ```
97
97
 
98
- `pnpm verify` is the check to run before you finish. Run `--full` after a change to shared components, the shell or data, and `--timed` on a quiet machine to judge the times; `pnpm verify --full --timed` is the whole standard. Every run ends with a coverage line, such as `coverage: default; browser ran; 3 routes; data configured 3/3; interaction configured 3/3; not run: audit, presses, keyboard, assistant, browserChecks, timing, live, vitals`, which says what ran and what did not. Nothing that did not run is reported as passed. `pnpm verify --perf` adds the 20-sample navigation protocol and reports it; a p95 over its budget is a `warn`, never a failure. The browser suites look inside open shadow roots, so a web component's content is measured, pressed and walked like any other; a custom element they cannot look inside is reported as `unmeasured:`, and a selector in `navigationChecks` that matches several elements prints a `note:`. `skills/zz-meridian/references/validation.md` says what each line means and what to do about it. The default is built to finish within two minutes; `--full`, `--timed` and `--perf` are what a team schedules, weekly for instance.
98
+ `pnpm verify` is the check to run before you finish. Run `--full` after a change to shared components, the shell or data, and `--timed` on a quiet machine to judge the times; `pnpm verify --full --timed` is the whole standard. Every run ends with a coverage line, such as `coverage: default; browser ran; 3 routes; data configured 3/3; interaction configured 3/3; not run: audit, presses, keyboard, assistant, browserChecks, timing, live, vitals`, which says what ran and what did not. Nothing that did not run is reported as passed. `pnpm verify --perf` is the 20-sample navigation protocol, which runs only in CI, split by route with `--shard <i>/<n>` across runners, and reports; a p95 over its budget is a `warn`, never a failure. On your machine, perf runs only narrowed, `node scripts/perf.ts --routes <route>`. The browser suites look inside open shadow roots, so a web component's content is measured, pressed and walked like any other; a custom element they cannot look inside is reported as `unmeasured:`, and a selector in `navigationChecks` that matches several elements prints a `note:`. `skills/zz-meridian/references/validation.md` says what each line means and what to do about it. The default is built to finish within two minutes; `--full`, `--timed` and `--perf` are what a team schedules, weekly for instance, each as a job of its own.
@@ -30,9 +30,9 @@ A tier is when a check runs, ordered by speed and scope. The first four are one
30
30
  - **quick**: `pnpm gate`. The static checks and the tests, with no browser and no build; `tests/deep-*` is left out. In Meridian's repository it runs `tests/cli-*` too, which `node scripts/gate.ts --without-cli-tests` leaves out.
31
31
  - **change**: `pnpm verify`. Adds to the gate (run without `tests/cli-*`) one production build, the route policy, the first-load and HTML size checks, and the navigation of up to three routes on desktop and through the phone drawer, judged on function. It ends with a coverage line that says what ran and what did not, and never refuses for a missing Chrome or safe backend: it says the browser did not run, and why.
32
32
  - **complete**: `pnpm verify --full`. Adds the navigation of every rail route, the audit of every page, every control pressed and every link followed, the keyboard walk, the assistant and the product's own browser checks. Nothing in it judges a time, so it is not the whole standard: `pnpm verify --full --timed` is.
33
- - **timed**: `pnpm verify --timed`. The navigation of every rail route with its budgets judged, then the live data and Web Vitals, alone and last, so it needs a quiet machine. It does not imply the complete tier ran. `pnpm verify --full --timed` is the whole standard, on one gate and one build with the complete suites first, and the only run that can end "the project meets the Meridian standard".
33
+ - **timed**: `pnpm verify --timed`. The navigation of every rail route with its budgets judged, then the live data, the product's own timed checks (`timedChecks` in `scripts/verify.config.ts`) and Web Vitals, alone and last, so it needs a quiet machine. It does not imply the complete tier ran. `pnpm verify --full --timed` is the whole standard, on one gate and one build with the complete suites first, and the only run that can end "the project meets the Meridian standard". The timed suites measure the server the run left: with the assistant off under `--timed` alone, and with it on, pointed at the fake model, under `--full --timed`, so a page there carries the assistant's launcher.
34
34
  - **on request**: a command no tier runs, run at the moment `## When each runs` gives it. The combinations that exist:
35
- - `pnpm verify --perf` adds the 20-sample navigation protocol to the tiers beside it, or to the change tier alone; `pnpm verify --full --timed --perf` runs all three on one gate and one build.
35
+ - `pnpm verify --perf` is the 20-sample navigation protocol, and the whole of it runs only in CI, split by route with `--shard <i>/<n>`, each shard on its own runner. It refuses to run elsewhere, and a shard runs alone or beside the other tiers. Locally, perf runs only narrowed, `node scripts/perf.ts --routes <route>`, to diagnose one route or to sample a recalibration.
36
36
  - `pnpm verify --as <persona>` signs the browser in as each persona it names (a comma-separated list), alone or beside `--full`. It is refused beside `--timed` or `--perf`, before anything runs.
37
37
  - `node scripts/audit.ts --atlas` and `node scripts/vitals.ts --atlas` run against a served app, outside `pnpm verify`, over every Design Atlas entry.
38
38
  - The browser-fixture tests of the checks, `pnpm exec vitest run tests/deep- --no-file-parallelism`, run beside `pnpm verify --full` after a change to a check script. Meridian's repository only.
@@ -50,9 +50,9 @@ A moment is when a person or an agent runs a command. A project's own CI may run
50
50
  - **Triggered by what changed**:
51
51
  - who may see or do what: `pnpm verify --as <persona>`, for each persona the change touches;
52
52
  - a card, a pattern or the Atlas: `node scripts/audit.ts --atlas` and, on a quiet machine, `node scripts/vitals.ts --atlas`;
53
- - a navigation budget in question: `pnpm verify --perf`, on a quiet machine;
53
+ - a navigation budget in question: `node scripts/perf.ts --routes <route>`, narrowed to the route, on a quiet machine;
54
54
  - a check script: the browser-fixture tests beside `pnpm verify --full` (Meridian's repository only).
55
- - **On a schedule**: the slow and statistical checks, `pnpm verify --full --timed --perf`, where the perf part is a report: a p95 over its budget is a warning and only a broken sample fails.
55
+ - **On a schedule**: the slow and statistical checks, each tier as its own job on its own runner: `pnpm verify --full`, `pnpm verify --timed` and `pnpm verify --perf --shard <i>/<n>`. The perf part is a report: a p95 over its budget is a warning and only a broken sample fails. The whole perf protocol runs only in CI: a project runs it in its own CI, split into shards on separate runners.
56
56
  - **Meridian's release** (`.github/workflows/release.yml`, dispatched from master; Meridian's repository only), each command once:
57
57
  - `pnpm exec vitest run tests/cli-` and `pnpm exec vitest run tests/deep- --no-file-parallelism`;
58
58
  - `node cli/scripts/release-check.ts --limit 180`: the default verify from a clean build, within 180 s on the shared 4-CPU runner;
@@ -60,7 +60,7 @@ A moment is when a person or an agent runs a command. A project's own CI may run
60
60
  - after the publish, a project created from the registry and one updated to it from the release before.
61
61
  - It never runs the complete, timed or perf suites: the weekly run does.
62
62
  - **Meridian's weekly run** (`.github/workflows/weekly.yml`, Mondays at 03:17 UTC and on demand, on master's current commit; Meridian's repository only):
63
- - `pnpm verify --full --timed --perf`, as on a schedule above;
63
+ - the jobs `full` (`pnpm verify --full`), `timed` (`pnpm verify --timed`) and `perf` (`pnpm verify --perf --shard <i>/5`, five shards), in parallel on separate runners, as on a schedule above; a red is rerun by its job, and a dispatch names its jobs with `-f jobs=timed`;
64
64
  - `node cli/scripts/smoke.ts --tarball <tarball> --adopt --update --origins 3 --update-all --verify`, on a tarball packed from that commit: the adopted project's default verify cases, the update from each of the last three releases, and every recovery and failure case of the update.
65
65
  - Nothing waits on it: a failure notifies.
66
66
 
@@ -73,7 +73,7 @@ A moment is when a person or an agent runs a command. A project's own CI may run
73
73
  - A red whose cause is not named after a run narrowed to its route or page, with the render instrumented, goes to the owner as a decision, with what was measured, before any limit is touched.
74
74
  - **Limits**: a navigation budget, a live-data bound, or the LCP or INP floor may move, and only this way.
75
75
  - It moves only after a trace of the narrowed run names where the time goes, and none of it is the page's own long task or request that a change in the page would remove.
76
- - The new value is the nearest-rank p95 of at least 20 samples on a quiet machine, taken with the phone the timed suites emulate, the larger of the local and the scheduled runner's where both exist, times 1.25, rounded up to the next 10 ms.
76
+ - The new value is the nearest-rank p95 of at least 20 samples on a quiet machine, taken with the phone the timed suites emulate, the larger of the CI protocol's p95 and a narrowed local run's where both exist, times 1.25, rounded up to the next 10 ms.
77
77
  - It is never more than twice the old value without the owner's word.
78
78
  - CLS and dropped frames are never moved: a page that misses them is fixed.
79
79
  - A moved limit is written where the limit lives (`scripts/lib/budgets.ts`, or the check), and recorded in a `## Budgets` section of this page, placed before `## The classification`: one row per limit with its old and new value, the run, machine and commit it was set from, the samples, the p95 and the margin.
@@ -93,9 +93,17 @@ A moment is when a person or an agent runs a command. A project's own CI may run
93
93
  - **Tiers** as stages: fast checks before every commit and slower stages after, in one pipeline (Humble and Farley, *Continuous Delivery*, 2010, the deployment pipeline's commit and acceptance stages). In a project, a person or an agent runs the later stages at their moments; only Meridian's release and weekly run are automated.
94
94
  - **Proportions**: many fast tests and few slow end-to-end ones ("[The Practical Test Pyramid](https://martinfowler.com/articles/practical-test-pyramid.html)", Ham Vocke, on Martin Fowler's site, 2018).
95
95
  - **Types** after the ISTQB glossary and ISO/IEC 25010. Meridian lifts performance out of the non-functional qualities into a type of its own, because it alone needs a quiet machine, and does not count regression as a type, since the ISTQB defines regression testing by why it runs.
96
- - **Measurement**: performance judged by percentiles of repeated samples on a quiet machine, the phone after Lighthouse's mobile throttling, the CPU slowed four times on Slow 4G ([Lighthouse's throttling guide](https://github.com/GoogleChrome/lighthouse/blob/main/docs/throttling.md)). The 390 px touch width, the 20 samples, the 1.25 margin and the rounding of `Limits` are Meridian's own.
96
+ - **Measurement**: performance judged by percentiles of repeated samples on a quiet machine, the phone after Lighthouse's mobile throttling, the device of benchmark index 800 with the CPU slowed by the host's own index over 800, on Slow 4G ([Lighthouse's throttling guide](https://github.com/GoogleChrome/lighthouse/blob/main/docs/throttling.md)). The benchmark times JavaScript, not style and layout, so two hosts of one index can differ in the second: on 2026-10-19 the scheduled runner spent about twice this machine's time in style and layout at the same rate. A dropped frame stays red wherever it is measured, and its cause in the vitals output says which part of the frame the time went to. The 390 px touch width, the 20 samples, the 1.25 margin and the rounding of `Limits` are Meridian's own.
97
97
  - **Flakiness** as a defect: a test that passes and fails on the same code erodes trust in every result (*Software Engineering at Google*, ch. 11 and 14). Where a large test's flakiness is tolerated or retried, Meridian does not clear a red by a retry.
98
98
 
99
+ ## Budgets
100
+
101
+ A limit that moved, with what it was set from (`## Rules`, Limits).
102
+
103
+ | Limit | Old | New | Run | Machine | Commit | Samples | p95 | Margin |
104
+ |---|---|---|---|---|---|---|---|---|
105
+ | `/system/start/start-a-dashboard` phone warm shell, its own `budget` in `scripts/verify.config.ts` | 300 ms | 250 ms, the shared phone warm shell: the route's own figure is withdrawn | weekly `perf` shard 5/5, run 39145209850 | GitHub's `ubuntu-24.04` runner, the phone at 3.2x | `dfdad67` (withdrawn in `55b7ece`) | 20 | 58 ms (median 43, max 59) | none: a withdrawal returns to the shared figure, which holds the p95 with room |
106
+
99
107
  ## The classification
100
108
 
101
109
  Every check Meridian runs has one row, named by a key: a gate step, a group of test files, a phase or suite of `pnpm verify`, or a standalone suite. In Meridian's repository the table is guarded by `tests/cli-test-tiers.test.ts`, which fails naming the key when a step, suite or group has no row or a row names something that does not exist. A run needs a quiet machine exactly when it runs a check whose type is performance.
@@ -108,27 +116,28 @@ Every check Meridian runs has one row, named by a key: a gate step, a group of t
108
116
  | `gate/the product's own rules hold` | static | non-functional | quick | no | `node scripts/check.local.ts` | 0 s at 0bee6d7 (not present in this repository: it runs only where a product wrote scripts/check.local.ts) | A product's own rules, beside Meridian's. |
109
117
  | `gate/contrast holds in every theme and accent` | static | non-functional | quick | no | `node scripts/contrast.ts` | 0.1 s at 0bee6d7 | Every specified foreground on its background, in every theme and accent. |
110
118
  | `gate/route types are generated` | static | non-functional | quick | no | `next typegen` | 0.2 s at 0bee6d7 | Route types are generated before the type check. |
111
- | `gate/types check` | static | functional | quick | no | `tsc --noEmit` | 2.7 s at 0bee6d7 | The TypeScript compiles. |
112
- | `gate/lint passes` | static | non-functional | quick | no | `eslint .` | 17.1 s at 0bee6d7 | The code passes the Next ESLint config. |
113
- | `gate/tests pass` | small, medium | functional | quick | no | `pnpm exec vitest run --exclude 'tests/deep-*'` | 68.8 s at 89e4ef4 | The tests outside `tests/deep-*` pass (run with the lint and the type check). |
119
+ | `gate/types check` | static | functional | quick | no | `tsc --noEmit` | 3.7 s at 40d29c7 | The TypeScript compiles. |
120
+ | `gate/lint passes` | static | non-functional | quick | no | `eslint .` | 22.5 s at 40d29c7 | The code passes the Next ESLint config. |
121
+ | `gate/tests pass` | small, medium | functional | quick | no | `pnpm exec vitest run --exclude 'tests/deep-*'` | 48.8 s at 40d29c7 | The tests outside `tests/deep-*` pass (run with the lint and the type check). |
114
122
  | `tests/shipped` | small, medium | functional | quick | no | `pnpm exec vitest run --exclude 'tests/deep-*' --exclude 'tests/cli-*'` | 42 s at 7e6de42 | The behaviour tests of the components, pages and data (summed file time). |
115
123
  | `tests/cli-*` | small, medium | functional | quick | no | `pnpm exec vitest run tests/cli-` | 124 s at 7e6de42 | Meridian's repository only. The package's own tests: the CLI, its update and release paths (summed file time). |
116
- | `tests/deep-*` | large | functional | complete | no | `pnpm exec vitest run tests/deep- --no-file-parallelism` | 743 s at 89e4ef4 | Meridian's repository only. The check scripts against browser fixtures: each planted defect is found and each corrected twin passes. |
117
- | `verify/build` | large | functional | change | no | `pnpm verify` | 6.9 s at bc1e10b | One production build. |
118
- | `verify/route policy` | large | non-functional | change | no | `pnpm verify` | 0.1 s at bc1e10b | Every route is declared static or dynamic as its policy says. |
119
- | `verify/first-load` | large | non-functional | change | no | `pnpm verify` | 0.3 s at bc1e10b | The first-load size of each route is within its budget. |
120
- | `verify/html` | large | non-functional | change | no | `pnpm verify` | 0.3 s at bc1e10b | The served HTML of each route is within its size check. |
121
- | `verify/navigation` | large | functional | change | no | `pnpm verify` | 88.5 s at 89e4ef4 | 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`. |
122
- | `verify/timing` | large | performance | timed | yes | `pnpm verify --timed` | 86.5 s at 89e4ef4 | The same navigation of every rail route, with each shell, data and interaction time judged against its budget. |
123
- | `verify/personas` | large | functional | on request | no | `pnpm verify --as viewer` | 104 s at 0bee6d7 | Signed in as each named persona, the rail shows the routes that persona may open and a denied route draws its refusal. |
124
- | `verify/audit` | large | non-functional | complete | no | `pnpm verify --full` | 626.6 s at bc1e10b | Every page and embed view at five widths in both themes, for layout and accessibility defects. |
125
- | `verify/presses` | large | functional | complete | no | `pnpm verify --full` | 998.8 s at bc1e10b | Every control pressed and every link followed, on desktop and phone. |
124
+ | `tests/deep-*` | large | functional | complete | no | `pnpm exec vitest run tests/deep- --no-file-parallelism` | 782 s at fa5bbff | Meridian's repository only. The check scripts against browser fixtures: each planted defect is found and each corrected twin passes. |
125
+ | `verify/build` | large | functional | change | no | `pnpm verify` | 9.6 s at 40d29c7 | One production build. |
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
+ | `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` | 0.3 s at 40d29c7 | The served HTML of each route is within its size check. |
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
+ | `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
+ | `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. |
132
+ | `verify/audit` | large | non-functional | complete | no | `pnpm verify --full` | 652.0 s at 269d307 | Every page and embed view at five widths in both themes, for layout and accessibility defects. |
133
+ | `verify/presses` | large | functional | complete | no | `pnpm verify --full` | 1006.9 s at 269d307 | Every control pressed and every link followed, on desktop and phone. |
126
134
  | `verify/keyboard` | large | functional | complete | no | `pnpm verify --full` | 205.6 s at bc1e10b | Tab through each page and back: focus rings, reach and order. |
127
135
  | `verify/assistant` | large | functional | complete | no | `pnpm verify --full` | 101.2 s at bc1e10b | The assistant absent and in a walk-through (54.7 s and 46.5 s). |
128
- | `verify/live` | large | performance | timed | yes | `pnpm verify --timed` | 40.3 s at 89e4ef4 | Two tabs, a silent stream, a restart, a hidden and an offline tab and a burst: each converges within its bound. |
129
- | `verify/vitals` | large | performance | timed | yes | `pnpm verify --timed` | 311.1 s at bc1e10b | LCP, INP, CLS and dropped frames on a mid-range phone for every static page. |
130
- | `verify/browserChecks` | large | functional | complete | no | `pnpm verify --full` | 27.8 s at bc1e10b | The product's own browser journeys (`journeys/workspaces.ts`). |
131
- | `verify/perf` | large | performance | on request | yes | `pnpm verify --perf` | 84.8 min at 89e4ef4 | The 20-sample navigation protocol. |
132
- | `suite/audit --atlas` | large | non-functional | on request | no | `node scripts/audit.ts --atlas` | 21.5 min at 89e4ef4 | The audit over the Design Atlas: 183 routes at 1440 and 390px in both themes. |
133
- | `suite/vitals --atlas` | large | performance | on request | yes | `node scripts/vitals.ts --atlas` | 16.3 min at 89e4ef4 | Web Vitals for every Atlas entry: 110 pages. |
134
- | `suite/consumer smoke` | large | functional | on request | no | `node cli/scripts/smoke.ts --tarball <tarball> --adopt --create --update` | 4.5 min at 89e4ef4 | Meridian's repository only. The release case: a packed tarball adopted, created and updated end to end. Release runs this command. Weekly runs `--adopt --update --origins 3 --update-all --verify`, whose `--verify` runs the adopted project's default verify cases. The created project's case, `--create --verify`, runs its `verify --full --timed`, runs on request only and wants a quiet machine the release case does not. |
136
+ | `verify/live` | large | performance | timed | yes | `pnpm verify --timed` | 41.1 s at fa5bbff | Two tabs, a silent stream, a restart, a hidden and an offline tab and a burst: each converges within its bound. |
137
+ | `verify/timedChecks` | large | performance | timed | yes | `pnpm verify --timed` | 0 s at fa5bbff | The product's own browser checks that judge a time, one at a time; the sample declares none. |
138
+ | `verify/vitals` | large | performance | timed | yes | `pnpm verify --timed` | 315.0 s at fa5bbff | LCP, INP, CLS and dropped frames on a mid-range phone for every static page. |
139
+ | `verify/browserChecks` | large | functional | complete | no | `pnpm verify --full` | 27.8 s at bc1e10b | The product's own browser journeys (`journeys/workspaces.ts`) that judge no time. |
140
+ | `verify/perf` | large | performance | on request | yes | `pnpm verify --perf --shard <i>/<n>` | 24.1 min at dfdad67, the slowest of five shards on the runner | The 20-sample navigation protocol. |
141
+ | `suite/audit --atlas` | large | non-functional | on request | no | `node scripts/audit.ts --atlas` | 22.0 min at fa5bbff | The audit over the Design Atlas: 183 routes at 1440 and 390px in both themes. |
142
+ | `suite/vitals --atlas` | large | performance | on request | yes | `node scripts/vitals.ts --atlas` | 16.2 min at fa5bbff | Web Vitals for every Atlas entry: 110 pages. |
143
+ | `suite/consumer smoke` | large | functional | on request | no | `node cli/scripts/smoke.ts --tarball <tarball> --adopt --create --update` | 3.0 min at 40d29c7 | Meridian's repository only. The release case: a packed tarball adopted, created and updated end to end. Release runs this command. Weekly runs `--adopt --update --origins 3 --update-all --verify`, whose `--verify` runs the adopted project's default verify cases. The created project's case, `--create --verify`, runs its `verify --full --timed`, runs on request only and wants a quiet machine the release case does not. |
@@ -261,7 +261,11 @@ const knownCustomers = customerRoutes.filter((r) => r !== unknownCustomer);
261
261
 
262
262
  /** The title the navigation check names for a rail route: what its page's heading shows (navigate.ts holds it to the same, by inclusion). */
263
263
  const titleOf = (href: string) => config.navigationChecks?.find((c) => c.path === href)?.title;
264
- const headingOf = (page: Page) => until("the page's heading", () => ask<string>(page, `const h = document.querySelector('main h1, h1'); const t = h ? text(h) : ''; return t || null;`));
264
+ /**
265
+ * The page's heading, once the page is there: while its `loading.tsx` stands in (a `role="status"` that is busy), the
266
+ * masthead holds the page's name where a record's name will be, and a step reading it then failed on a loaded machine.
267
+ */
268
+ export const headingOf = (page: Page) => until("the page's heading", () => ask<string>(page, `if (document.querySelector('[role="status"][aria-busy="true"]')) return null; const h = document.querySelector('main h1, h1'); const t = h ? text(h) : ''; return t || null;`));
265
269
  const mainText = (page: Page) => ask<string>(page, `const m = document.querySelector('main'); return m ? text(m) : '';`);
266
270
 
267
271
  /**
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zz-meridian-template",
3
- "version": "0.17.0",
3
+ "version": "0.18.0",
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.",
@@ -313,6 +313,13 @@ const run = async (route: string, width: number, theme: string, embed: boolean)
313
313
  }
314
314
  // The page's late chrome first (scripts/lib/settle.ts): what arrives after the measurement is taken is never measured.
315
315
  await settle(page);
316
+ // A subtree with content-visibility: auto that is off screen is not laid out: the browser skips it, and checkVisibility()
317
+ // does not count it as off (it is not hidden, only not laid out yet), so the walk measured boxes that were never laid
318
+ // out. Render every such subtree first and let two frames draw it. (The probe found 180 issues across 9 routes as
319
+ // shipped and 0 with every auto element rendered first.) content-visibility: hidden is left alone: it is not drawn,
320
+ // and checkVisibility() already answers for it.
321
+ await page.eval(`(() => { for (const n of deepAll(document.body)) if (n instanceof HTMLElement && getComputedStyle(n).contentVisibility === 'auto') n.style.contentVisibility = 'visible'; return true; })()`);
322
+ await page.eval(`new Promise((done) => requestAnimationFrame(() => requestAnimationFrame(() => done(true))))`);
316
323
  const r = await page.eval<Report>(MEASURE);
317
324
  // Focus: press Tab through the page as a person would; every stop must show a ring (outline or box-shadow ring) on the
318
325
  // focused element or the frame around it.