zz-meridian 0.16.3 → 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.
- package/README.md +6 -6
- package/dist/context.js +2 -1
- package/dist/create.js +1 -1
- package/dist/migrations.js +17 -9
- package/package.json +1 -1
- package/payload/AGENTS.md +1 -1
- package/payload/CHANGELOG.md +77 -0
- package/payload/CONTRIBUTING.md +15 -11
- package/payload/README.md +16 -14
- package/payload/app/(dashboard)/(home)/README.md +2 -0
- package/payload/app/(dashboard)/business/README.md +1 -1
- package/payload/app/(dashboard)/engineering/page.tsx +1 -1
- package/payload/app/(dashboard)/keys/page.tsx +2 -1
- package/payload/app/(dashboard)/members/page.tsx +2 -2
- package/payload/app/(dashboard)/operations-tickets/page.tsx +2 -1
- package/payload/app/(dashboard)/releases/page.tsx +2 -2
- package/payload/app/(dashboard)/tests/page.tsx +2 -2
- package/payload/app/api/live/route.ts +19 -1
- package/payload/docs/agents.md +2 -0
- package/payload/docs/benchmark.md +1 -1
- package/payload/docs/distribution.md +13 -11
- package/payload/docs/skill-evals.md +1 -1
- package/payload/docs/start-a-dashboard.md +4 -3
- package/payload/docs/testing.md +143 -0
- package/payload/journeys/workspaces.ts +5 -1
- package/payload/package.json +1 -1
- package/payload/scripts/ask.ts +77 -27
- package/payload/scripts/audit.ts +7 -0
- package/payload/scripts/brand.ts +1 -1
- package/payload/scripts/fake-llm.ts +22 -11
- package/payload/scripts/gate.ts +13 -6
- package/payload/scripts/interactions.ts +28 -7
- package/payload/scripts/lib/budgets.ts +22 -1
- package/payload/scripts/lib/chrome.ts +5 -0
- package/payload/scripts/lib/config-hooks.ts +17 -0
- package/payload/scripts/lib/coverage.ts +23 -6
- package/payload/scripts/lib/frame-cause.ts +41 -0
- package/payload/scripts/lib/journey.ts +20 -8
- package/payload/scripts/lib/links.ts +14 -0
- package/payload/scripts/lib/phone.ts +54 -0
- package/payload/scripts/lib/routes.ts +37 -10
- package/payload/scripts/lib/shard.ts +23 -0
- package/payload/scripts/lib/sizes.ts +8 -2
- package/payload/scripts/live.ts +6 -6
- package/payload/scripts/navigate.ts +14 -14
- package/payload/scripts/perf.ts +18 -7
- package/payload/scripts/shot.ts +22 -4
- package/payload/scripts/verify.baseline.json +29 -29
- package/payload/scripts/verify.config.ts +15 -7
- package/payload/scripts/verify.ts +91 -31
- package/payload/scripts/vitals.ts +32 -21
- package/payload/skills/zz-meridian/SKILL.md +28 -23
- package/payload/skills/zz-meridian/references/cache.md +13 -8
- package/payload/skills/zz-meridian/references/customize.md +18 -18
- package/payload/skills/zz-meridian/references/existing-project.md +19 -15
- package/payload/skills/zz-meridian/references/live.md +55 -9
- package/payload/skills/zz-meridian/references/standard.md +25 -23
- package/payload/skills/zz-meridian/references/update.md +18 -14
- package/payload/skills/zz-meridian/references/validation.md +60 -43
- package/payload/src/app.config.ts +6 -2
- package/payload/src/components/base/shell/README.md +1 -1
- package/payload/src/components/base/shell/index.tsx +25 -12
- package/payload/src/components/base/use-nav-drawer.ts +31 -3
- package/payload/src/components/patterns/featured-metric/README.md +5 -2
- package/payload/src/components/patterns/featured-metric/index.tsx +34 -11
- package/payload/src/components/patterns/featured-metric/preview.tsx +9 -0
- package/payload/src/components/patterns/metric-tile/README.md +1 -1
- package/payload/src/components/patterns/metric-tile/index.tsx +2 -1
- package/payload/src/components/patterns/rail/README.md +2 -2
- package/payload/src/components/patterns/rail/index.tsx +6 -0
- package/payload/src/components/ui/segmented/README.md +2 -0
- package/payload/src/components/ui/segmented/index.tsx +8 -7
- package/payload/src/data/README.md +1 -1
- package/payload/src/data/engineering.ts +10 -5
- package/payload/src/data/live-stream.ts +12 -4
- package/payload/src/data/metrics.ts +38 -1
- package/payload/src/data/read.ts +13 -8
- package/payload/src/data/tickets.ts +4 -4
- package/payload/src/lib/assistant/prompt.ts +15 -3
- package/payload/src/lib/assistant/respond.ts +1 -1
- package/payload/src/lib/collection.ts +18 -3
- package/payload/src/lib/live.ts +90 -22
- package/payload/src/styles/base.css +12 -0
- package/payload/src/system/atlas-shell.tsx +7 -6
- package/payload/src/system/card-stage.tsx +2 -2
- package/payload/src/system/page-stage.tsx +7 -2
- package/payload/src/views/home.tsx +4 -1
- package/payload/src/views/keys.tsx +3 -3
- package/payload/src/views/members.tsx +3 -3
- package/payload/src/views/now.tsx +5 -5
- package/payload/src/views/releases.tsx +3 -3
- package/payload/src/views/revenue.tsx +4 -1
- package/payload/src/views/services-context.ts +10 -1
- package/payload/src/views/services.tsx +2 -3
- package/payload/src/views/tests.tsx +3 -3
- package/payload/src/views/tickets.tsx +4 -3
- package/payload/src/views/tools.ts +11 -11
- package/payload/tests/assistant-prompt-writes.test.ts +69 -0
- package/payload/tests/assistant-withheld.test.ts +1 -1
- package/payload/tests/explore-runner.test.ts +6 -4
- package/payload/tests/featured-metric.test.tsx +72 -0
- package/payload/tests/live-client.test.tsx +78 -0
- package/payload/tests/live-resume.test.ts +65 -0
- package/payload/tests/live-revision-boot.test.ts +20 -0
- package/payload/tests/metric-tile-title.test.tsx +14 -0
- package/payload/tests/segmented-measure.test.tsx +40 -0
- package/payload/tests/shell-drawer-open.test.tsx +49 -0
- package/payload/tests/verify-suite-outcome.test.ts +4 -4
- package/payload/tests/view-tool-modules.test.ts +50 -0
- package/payload/tests/workspace-data-paths.test.ts +1 -1
package/README.md
CHANGED
|
@@ -13,11 +13,11 @@ words at the end:
|
|
|
13
13
|
|
|
14
14
|
In Claude Code or Codex, send it as a goal (`/goal Run npx zz-meridian@latest skill --global, then …`): the agent keeps
|
|
15
15
|
working until the condition holds, and a separate model checks it after every turn against the hand-over, which quotes
|
|
16
|
-
its evidence. The sentence says what you want; the skill carries how good it has to be. It builds, renders every view
|
|
17
|
-
|
|
18
|
-
and craft (judged on the renders), and improves it round by round until nothing is weak,
|
|
19
|
-
and recording each decision it made for you. You get the finished product, the renders,
|
|
20
|
-
can answer.
|
|
16
|
+
its evidence. The sentence says what you want; the skill carries how good it has to be. It builds, renders every view at
|
|
17
|
+
phone and desktop width in both themes, scores each one on a scoreboard of floors (measured by
|
|
18
|
+
`pnpm verify --full --timed`) and craft (judged on the renders), and improves it round by round until nothing is weak,
|
|
19
|
+
deciding what it can find out and recording each decision it made for you. You get the finished product, the renders,
|
|
20
|
+
and the few questions only you can answer.
|
|
21
21
|
|
|
22
22
|
The skill picks the command from what you said and what is in the folder, and names it before running anything:
|
|
23
23
|
`adopt` to change this Next.js App Router project in place ("make this admin panel look professional"), `create` for a
|
|
@@ -80,7 +80,7 @@ styles) and `src/app.config.ts` for the new flags and records them in the manife
|
|
|
80
80
|
refuses when one of those outputs was edited, while an update is open, or on a dirty tree (unless `--allow-dirty`). Run
|
|
81
81
|
it with the version the manifest records. The project's own `pnpm brand` still works, but its changes count as your edits.
|
|
82
82
|
|
|
83
|
-
Requires Node 22.18 or newer. The default `pnpm verify` runs without Google Chrome and reports the browser checks as not run; `--full` and `--perf` need it.
|
|
83
|
+
Requires Node 22.18 or newer. The default `pnpm verify` runs without Google Chrome and reports the browser checks as not run; `--full`, `--timed` and `--perf` need it.
|
|
84
84
|
|
|
85
85
|
## What this package does not do
|
|
86
86
|
|
package/dist/context.js
CHANGED
|
@@ -19,7 +19,8 @@ When it is absent, use the skill's brief template with the person's answers; do
|
|
|
19
19
|
upgrade: resolve the report and run the pinned finalization command.
|
|
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
|
-
Use \`pnpm verify --full\`
|
|
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. 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.
|
|
23
24
|
- Agents in the product read through authorized collections and write only through an approved Proposal.
|
|
24
25
|
${END}`;
|
|
25
26
|
/** The brief a team keeps at `docs/brief.md`; its guidance lines equal `BRIEF_GUIDANCE` in the assistant prompt. */
|
package/dist/create.js
CHANGED
|
@@ -77,7 +77,7 @@ ${name} is a new Meridian ${VERSION} dashboard in ${root}.
|
|
|
77
77
|
skill: .agents/skills/zz-meridian (Codex) and .claude/skills/zz-meridian (Claude Code)
|
|
78
78
|
|
|
79
79
|
Next: follow .agents/skills/zz-meridian/SKILL.md from step 3's brief (the template is fetched and branded): the brief,
|
|
80
|
-
step 4's colour check, then data and the pages, then the loop in references/standard.md until ${pm} run verify --full ends with "the project meets the
|
|
80
|
+
step 4's colour check, then data and the pages, then the loop in references/standard.md until ${pm} run verify --full --timed ends with "the project meets the
|
|
81
81
|
Meridian standard" and no view on the scoreboard is Weak.`);
|
|
82
82
|
return 0;
|
|
83
83
|
}
|
package/dist/migrations.js
CHANGED
|
@@ -235,8 +235,8 @@ export const RELEASE_MIGRATIONS = [
|
|
|
235
235
|
id: 'verify-modes',
|
|
236
236
|
since: '0.5.0',
|
|
237
237
|
applies: removedVerifyFlags,
|
|
238
|
-
summary: 'verify has
|
|
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 and `pnpm verify --perf
|
|
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, 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
|
|
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
|
{
|
|
@@ -454,6 +454,14 @@ export const RELEASE_MIGRATIONS = [
|
|
|
454
454
|
instructions: "Replace `clock().toISOString().slice(0, 10)` with `dayOf(clock())` (`dayOf` in `src/lib/format-date.ts`) in each file listed: the prepared query's `today` in `app/api/assistant/route.ts`, and `readDays`'s today in `src/data/metrics.ts`. The prompt has told the model today in `app.timezone` all along, while the prepared query and the page cut their periods at the UTC date, so east of UTC after local midnight (west of UTC in the evening) the model's today and its `7d` window's end were a day apart. A day row is kept under its day in `app.timezone`, so a file of your own that compares an instant's day with one — `now.slice(0, 10)`, `at.slice(0, 10)` — asks `dayOf` too. A product whose `app.timezone` is UTC sees no change. A project that kept the sample's views brings `src/views/now-parts.ts`, `src/views/revenue-parts.ts`, `src/views/customer-context.ts`, `src/views/customer.tsx` and `src/views/members.tsx` over from the release.",
|
|
455
455
|
checks: ['gate'],
|
|
456
456
|
},
|
|
457
|
+
{
|
|
458
|
+
id: 'verify-tiers',
|
|
459
|
+
since: '0.17.0',
|
|
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
|
+
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
|
+
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
|
+
checks: ['gate'],
|
|
464
|
+
},
|
|
457
465
|
];
|
|
458
466
|
/**
|
|
459
467
|
* A test of the rail that stands over a fake `ResizeObserver` answering `observe` with nothing (0.16.2). The lookbehind
|
|
@@ -472,15 +480,15 @@ function removedExportReaders(root) {
|
|
|
472
480
|
}));
|
|
473
481
|
}
|
|
474
482
|
/**
|
|
475
|
-
* The specifications of the pages the view tools
|
|
476
|
-
*
|
|
477
|
-
* every
|
|
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.
|
|
478
487
|
*/
|
|
479
488
|
function viewToolPagesWithoutAgentDoes(root) {
|
|
480
|
-
|
|
481
|
-
if (tools === null)
|
|
489
|
+
if (text(root, 'src/views/tools.ts') === null)
|
|
482
490
|
return null;
|
|
483
|
-
const calls = viewToolCalls(
|
|
491
|
+
const calls = teamSources(root).filter((f) => f.rel.startsWith('src/')).flatMap((f) => viewToolCalls(f.src)).filter((k) => !k.has('...'));
|
|
484
492
|
if (calls.length === 0)
|
|
485
493
|
return null;
|
|
486
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.
|
|
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",
|
package/payload/AGENTS.md
CHANGED
|
@@ -15,6 +15,6 @@ Meridian is a dashboard design system and template: read `README.md` (what it is
|
|
|
15
15
|
- **Build up, never sideways.** A page arranges patterns; a pattern composes components; a value is a token. Never write a colour, size or shadow that is not a token; `node scripts/check.ts` fails on literal colours and on Tailwind utilities outside Meridian's reset scales (they render nothing).
|
|
16
16
|
- **Tokens** live in `tokens/*.tokens.json` (DTCG 2025.10). `src/styles/tokens.css` and `src/styles/theme.css` are generated: run `pnpm tokens`, never edit them.
|
|
17
17
|
- **A card** is a folder with `index.tsx`, `preview.tsx` (every state, static, labelled) and `README.md` (the anatomy in CONTRIBUTING). After adding one, run `pnpm registry`.
|
|
18
|
-
- **Before finishing**: `pnpm verify` (the gate once, one build, the size checks and a smoke of up to three routes; it ends with a coverage line that says what ran and what did not). For a change to shared components, the shell or data, run `pnpm verify --full`: every route, every control pressed and every link followed, the keyboard walk, the assistant and the live data. Then look at `node scripts/shot.ts <route> --width 1440,390 --theme dark,light`. `pnpm gate` alone is the fast loop while editing.
|
|
18
|
+
- **Before finishing**: `pnpm verify` (the gate once, one build, the size checks and a smoke of up to three routes; it ends with a coverage line that says what ran and what did not). For a change to shared components, the shell or data, run `pnpm verify --full`: every route, every control pressed and every link followed, the keyboard walk, the assistant and the product's own checks; it judges no time. `pnpm verify --full --timed` adds the navigation budgets, the live data and Web Vitals on a quiet machine, and is the whole standard. Then look at `node scripts/shot.ts <route> --width 1440,390 --theme dark,light`. `pnpm gate` alone is the fast loop while editing. `docs/testing.md` is the testing standard: its `## When each runs` says which command runs at which moment, and its `## Rules` say when a red is fixed and when a limit may move.
|
|
19
19
|
- **Agents in the product** read freely and write only through a Proposal; mark agent work with the Agent mark and "via". See `docs/agents.md`.
|
|
20
20
|
- Everything written to the repository is in English.
|
package/payload/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,83 @@
|
|
|
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
|
+
|
|
46
|
+
## [0.17.0] · 2026-10-18
|
|
47
|
+
|
|
48
|
+
### Breaking
|
|
49
|
+
|
|
50
|
+
- **`pnpm verify --full` no longer judges a time.** It runs the complete suites: every rail route navigated for its function, the audit, every control pressed and link followed, the keyboard walk, the assistant and the product's own browser checks. The navigation budgets, the live-data suite and Web Vitals moved to the new `pnpm verify --timed`, which runs them alone and last and needs a quiet machine. Only `pnpm verify --full --timed` can end `the project meets the Meridian standard`; a `--full` run with nothing else unrun ends `every check that ran passed; not run: timing, live, vitals; pnpm verify --timed runs them`. A script, workflow or agent prompt that ran `--full` for the whole standard runs `--full --timed` instead; weekly CI runs `pnpm verify --full --timed --perf`. `docs/testing.md` classifies every check by size, type and tier.
|
|
51
|
+
- **`pnpm verify` and `--full` no longer fail a route for being slow.** Their navigation (`scripts/navigate.ts --timing off`) judges whether each route's shell, data and first interaction arrive, never how fast; `pnpm verify --timed` judges the same journeys against `budgets.navigation`. A project that relied on the default smoke to catch a slow route runs `pnpm verify --timed` on a quiet machine.
|
|
52
|
+
- **`pnpm gate` no longer runs `tests/deep-*`**, the browser-fixture tests of the checks, which only this repository has. Its closing line names the command that does: `pnpm exec vitest run tests/deep- --no-file-parallelism`.
|
|
53
|
+
- **The coverage line has new words.** Its depth can be `timed`, `full+timed` or `full+timed+perf`, and its `not run:` list can name `timing`. A script that matched `coverage: full;` for a complete run matches `coverage: full+timed;` and `not run: none`.
|
|
54
|
+
- **`finalOutcome` (`scripts/lib/coverage.ts`) takes `{ full, timed }` in place of a boolean.** A project's own copy of `tests/verify-suite-outcome.test.ts` from before the tiers calls `finalOutcome(true, …)` and failed its update's gate at the type check; the update now reports it as the `verify-tiers` migration. Bring `tests/verify-suite-outcome.test.ts` over from the release, and call `finalOutcome({ full, timed }, notRun)` in a file of your own.
|
|
55
|
+
|
|
56
|
+
### New
|
|
57
|
+
|
|
58
|
+
- **A Featured metric can say its period has no number** ([#45](https://github.com/zhixuan312/zz-meridian/issues/45)). `FeaturedFigure.value` is `number | null`, and `points` takes `null` entries. With no number, the figure's line holds `missing`, the caller's words ("No words recorded", "Unavailable"; "Not recorded" by default), a size down and at the figure's own height, so a period without a number draws the same card as one with. A pointed point that is `null` reads the same way. Before this, the only ways through were a false `0`, or `NaN` with a `format` that turned it into a word; a card that did the second can pass `null` and `missing` now.
|
|
59
|
+
|
|
60
|
+
### Changed
|
|
61
|
+
|
|
62
|
+
- **`docs/testing.md` is the testing standard.** Above its classification it says what kinds of test there are (size and type), the tiers that combine them, when each runs (while editing, before a commit, before finishing and handing over a change, before a release, when a given kind of change is made, and on a schedule), and the rules a red, a limit and a release follow, with the practice they rest on. A project is advised to follow it and Meridian follows it. `AGENTS.md`, `CONTRIBUTING.md`, `docs/distribution.md` and `references/validation.md` point to it instead of keeping their own account, and `tests/cli-testing-standard.test.ts` keeps its shape.
|
|
63
|
+
|
|
64
|
+
### Fixed
|
|
65
|
+
|
|
66
|
+
- **The assistant is told it changes nothing where it is handed no write tool** ([#46](https://github.com/zhixuan312/zz-meridian/issues/46)). The system prompt always said "Your write tools change what this page shows…" and that the person approves each Proposal, even on a page whose collections the assistant may only read, where no write tool is built and no Proposal can follow. `respond` now tells `systemPrompt` whether it handed a write tool (`Audience.writes`). Without one, the prompt says the assistant changes nothing and, asked for any change, gives the page that makes it, or says it is not a change the person can make. With one, it keeps both lines. The shared guidance that no change exists unless a write tool returned one stays in both.
|
|
67
|
+
- **`ask` writes one transcript a page and can ask a detail page on the record you name** ([#47](https://github.com/zhixuan312/zz-meridian/issues/47)). A page with no view tool was named by its folder, a detail page by the folder before its `[id]`, so `/things` and `/things/[id]` both wrote `things.md` and the second overwrote the first. A page without a tool is now named by its whole route (`/things/[id]` is `things-id`), two views that would share a name are told apart by their routes, and a name still shared is refused before anything is asked. `--view` takes the assistant's tool name as well (`view_usage`), and `--path <address>` asks the page that renders that address on that record, rather than on the first row its listing draws, which may be a closed one. The view names from tools defined in modules the registry imports came with #43. A transcript of a page without a tool has a new file name: compare the new file with the old one, under its new name.
|
|
68
|
+
- **Pointing at a Featured metric's chart moves nothing below the figure** ([#44](https://github.com/zhixuan312/zz-meridian/issues/44)). A figure given `points` and no `delta` drew the row under the figure only while a point was pointed at, so the card grew by the point pill's row as the pointer crossed its chart and shrank when it left: measured at 390 px, 380 → 426 px, and a control below moved 46 px with it, enough for a press aimed at it to land on something else. The row is now drawn at rest as a line of its own, holding the pill's place empty, so the card stays 426 px either way however long the point's name.
|
|
69
|
+
- **The gate and `ask` find a view tool kept in a module of its own** ([#43](https://github.com/zhixuan312/zz-meridian/issues/43)). `viewToolPages()` (`scripts/lib/routes.ts`) read the `defineViewTool` calls written in `src/views/tools.ts` alone, so a project that keeps each tool in a module the registry imports got an empty list. `scripts/check.ts` then asked no page for its `### What the agent does here` — green by absence — and `scripts/ask.ts` named every view by its page's folder, never by its tool. It now reads every project module the registry reaches through its imports, by a relative path or a tsconfig alias, an `index` included. A project whose tools sat in such modules can find pages failing `check.ts` that passed before: each is a page a tool names whose specification lacks the part, and the fix is that part.
|
|
70
|
+
- **Home and Revenue are live pages.** Each body registers the collections its feature needs with `useLive`, so `/` and `/business` resync on open, on reconnect and on the safety poll, and `pnpm verify --perf` finds an after-live start for every Home and Business route again: since the rails became per workspace, `/members`, `/customers`, `/usage` and `/requests` had none (`no other rail route registers live data to start from`).
|
|
71
|
+
- **A link probe is pressed once the destination's data is measured.** The navigation measured the data and pressed the probe side by side, so on a route whose link is ready before its ready text (`/business`, while its title still loaded) the press could leave before the data counted, and a sample failed `main is there but never shows`. A dead link is still reported.
|
|
72
|
+
- **Switching an Atlas card's theme no longer drops a frame on a phone.** The stage's specimen sections are skipped by style and layout until they near the viewport (`content-visibility: auto`), so the Theme, Accent, Density and Width switches restyle only what is on screen; a chart pattern's specimen holds thousands of elements, and the switch cost one frame of 100 to 160 ms on a mid-range phone.
|
|
73
|
+
- **`pnpm verify --perf` holds a route to the budget its navigation check carries**, as `pnpm verify --timed` already did. A route's own figure (`budget` on its check in `verify.config.ts`) now stands in for the shared warm figure in both suites, through one function (`limitsFor` in `scripts/lib/budgets.ts`), so the two no longer judge one route against two bars: the start guide's phone warm shell is reported against its own 300 ms, not the shared 250 ms. Cold and after-live keep the shared figures.
|
|
74
|
+
- **The Atlas's Contributing page no longer links to a page that answers 404.** It linked `docs/testing.md` relatively, which resolved to `/system/system/docs/testing.md`; the file is now named in backticks, as every other Atlas document names a file, and `tests/cli-atlas-doc-links.test.ts` keeps every document the Atlas serves free of relative links.
|
|
75
|
+
|
|
76
|
+
### Known issues
|
|
77
|
+
|
|
78
|
+
- **`node scripts/audit.ts --atlas` reports overlaps on the Atlas pattern pages whose specimen renders with `content-visibility: auto`** (featured metric, metric tile, column chart, heatmap and the records template). The pages draw nothing overlapping: the audit measures sections the browser has not laid out yet. Fixed in the next release. `pnpm verify` and its tiers are not affected.
|
|
79
|
+
- **`node scripts/vitals.ts --atlas` can drop one frame on a page specification's look control** (`/system/pages/revenue`, one run in three), where the stage restyles its console and phone frames in one tap. Fixed in the next release. `pnpm verify --timed` is not affected.
|
|
80
|
+
|
|
81
|
+
|
|
5
82
|
## [0.16.3] · 2026-10-17
|
|
6
83
|
|
|
7
84
|
### Breaking
|
package/payload/CONTRIBUTING.md
CHANGED
|
@@ -82,25 +82,29 @@ Write the status on the line under the title: `Status: beta`.
|
|
|
82
82
|
|
|
83
83
|
## Tools
|
|
84
84
|
|
|
85
|
+
`docs/testing.md` is the testing standard: the kinds of test, the tiers that combine them, when each runs, the rules a red, a limit and a release follow, and each check's size, type, tier and cost. The When column below uses its moments.
|
|
86
|
+
|
|
85
87
|
| Command | What it does | When |
|
|
86
88
|
|---|---|---|
|
|
87
|
-
| `pnpm gate` | Every check below, in order | Before every commit |
|
|
89
|
+
| `pnpm gate` | Every check below, in order; the deep tests (`tests/deep-*`) are left out | Before every commit |
|
|
88
90
|
| `pnpm tokens` | Generates the token CSS and the Tailwind bridge from `tokens/` (`--check` only compares) | After any token change |
|
|
89
91
|
| `node scripts/contrast.ts [--all]` | Every specified foreground on its background, in every theme and accent, and the chart palette's colour-vision checks | Gate |
|
|
90
92
|
| `node scripts/registry.ts` | Regenerates the Atlas registry from the card folders (`--check` only compares) | After adding or renaming a card |
|
|
91
93
|
| `rm -rf .next` before the next build, after editing a card's `README.md` | A production build in a working `.next` can serve the prerendered copy of the old text while the client hydrates the new one, which React reports as error #418 on that Atlas page. `pnpm dev` has no such copy, and a fresh clone (CI, a deployment) has none either | After changing what a card's page says |
|
|
92
94
|
| `node scripts/check.ts` | Every card has its README and preview, every README follows the anatomy, no literal colour in components, every token a spec names exists and is written with its own value, every embed view a document offers is a route, an element that claims a control's role has a key handler, and the specification of every page with a view tool has its **What the agent does here**, with its Protagonist line and three questions in their form | Gate |
|
|
93
95
|
| `pnpm typecheck`, `pnpm test` | TypeScript and the behaviour tests | Gate |
|
|
94
|
-
| `node scripts/audit.ts` | With the app running: every page and embed view at 2560, 1440, 1024, 768 and 390px in both themes (embeds at 720 and 420 on a simulated host ground). It measures inside open shadow roots too. Fails on sideways scroll, text clipped without an ellipsis, text cut with an ellipsis that keeps the whole of it in no `title`, two pieces of text drawn in the same place or a stroked line drawn through one, a control with no accessible name, more than one page scroller, rendered text under its WCAG minimum (an SVG's text too, read by its `fill` against what the SVG paints behind it), a Tab stop with no focus ring (a change of outline, shadow or border when focus arrives), a custom element it cannot look inside (`unmeasured:`), and any uncaught exception or console error. Prints the design metrics: type sizes, weights, radii and the hierarchy ratio |
|
|
95
|
-
| `node scripts/audit.ts --atlas` | With the app running: the same audit over the Design Atlas, at 1440 and 390px in both themes: every card's page and its bare preview, and every other entry the Atlas serves (the token views, the page specs, the guides). Its first line says what it reached, by kind, and `--list` prints the routes without opening a browser. Not part of `pnpm verify`, which stays fast: a card's preview is where a defect in its own box shows, so run it after changing a component | After
|
|
96
|
-
| `node scripts/vitals.ts [--atlas]` | With a production build running: LCP, INP, CLS and dropped frames on a mid-range phone for every static page, the Atlas's front page and one card page among them. `--atlas` measures every Atlas entry instead, about fifteen minutes, and is not part of `pnpm verify` | Inside `pnpm verify --
|
|
97
|
-
| `node scripts/interactions.ts` | With the app running: presses every control on every page (mouse at 1440px, taps at 390px) and follows every link. Reaches controls inside open shadow roots. Fails on a control that changes nothing, one something covers, and a link that answers 4xx |
|
|
98
|
-
| `node scripts/keyboard.ts` | With the app running: Tab through each page until focus comes back round, then Shift+Tab back. Fails on a stop with no focus ring, a control hidden under the sticky bar, a visible control never reached, and a reverse order that differs |
|
|
96
|
+
| `node scripts/audit.ts` | With the app running: every page and embed view at 2560, 1440, 1024, 768 and 390px in both themes (embeds at 720 and 420 on a simulated host ground). It measures inside open shadow roots too. Fails on sideways scroll, text clipped without an ellipsis, text cut with an ellipsis that keeps the whole of it in no `title`, two pieces of text drawn in the same place or a stroked line drawn through one, a control with no accessible name, more than one page scroller, rendered text under its WCAG minimum (an SVG's text too, read by its `fill` against what the SVG paints behind it), a Tab stop with no focus ring (a change of outline, shadow or border when focus arrives), a custom element it cannot look inside (`unmeasured:`), and any uncaught exception or console error. Prints the design metrics: type sizes, weights, radii and the hierarchy ratio | Inside `pnpm verify --full` |
|
|
97
|
+
| `node scripts/audit.ts --atlas` | With the app running: the same audit over the Design Atlas, at 1440 and 390px in both themes: every card's page and its bare preview, and every other entry the Atlas serves (the token views, the page specs, the guides). Its first line says what it reached, by kind, and `--list` prints the routes without opening a browser. Not part of `pnpm verify`, which stays fast: a card's preview is where a defect in its own box shows, so run it after changing a component | After a change to a card, a pattern or the Atlas |
|
|
98
|
+
| `node scripts/vitals.ts [--atlas]` | With a production build running: LCP, INP, CLS and dropped frames on a mid-range phone for every static page, the Atlas's front page and one card page among them. `--atlas` measures every Atlas entry instead, about fifteen minutes, and is not part of `pnpm verify` | Inside `pnpm verify --timed`; `--atlas` after a change to a card, a pattern or the Atlas, on a quiet machine |
|
|
99
|
+
| `node scripts/interactions.ts` | With the app running: presses every control on every page (mouse at 1440px, taps at 390px) and follows every link. Reaches controls inside open shadow roots. Fails on a control that changes nothing, one something covers, and a link that answers 4xx | Inside `pnpm verify --full` |
|
|
100
|
+
| `node scripts/keyboard.ts` | With the app running: Tab through each page until focus comes back round, then Shift+Tab back. Fails on a stop with no focus ring, a control hidden under the sticky bar, a visible control never reached, and a reverse order that differs | Inside `pnpm verify --full` |
|
|
101
|
+
| `pnpm exec vitest run tests/deep- --no-file-parallelism` | The browser-fixture tests of the check scripts: each planted defect is found and each corrected twin passes | After a change to a check script, beside `pnpm verify --full`; Meridian's release runs it |
|
|
99
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 |
|
|
100
|
-
| `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
|
|
101
|
-
| `pnpm verify --
|
|
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
|
+
| `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, 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 |
|
|
102
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 |
|
|
103
|
-
| `node scripts/shot.ts <routes> --width 1440,390 --theme light,dark [--full]` | Screenshots into `out/shots/` | While
|
|
107
|
+
| `node scripts/shot.ts <routes> --width 1440,390 --theme light,dark [--full]` | Screenshots into `out/shots/` | While editing |
|
|
104
108
|
|
|
105
109
|
A check is code, and it is wrong in the ways code is. Before a new check, or a change to one, is trusted:
|
|
106
110
|
|
|
@@ -112,8 +116,8 @@ A check is code, and it is wrong in the ways code is. Before a new check, or a c
|
|
|
112
116
|
## Releasing
|
|
113
117
|
|
|
114
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.
|
|
115
|
-
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.
|
|
116
|
-
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
|
|
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`, `verify --timed` and `verify --perf` run weekly, each as its own job (`.github/workflows/weekly.yml`), not here.
|
|
117
121
|
|
|
118
122
|
The `/release` command (`.claude/commands/release.md`) is this procedure as a runbook.
|
|
119
123
|
|
package/payload/README.md
CHANGED
|
@@ -77,8 +77,8 @@ Dark is the default, written on `:root`; the light theme follows the operating s
|
|
|
77
77
|
| `scripts/` | Generators, gates, `brand.ts` and `verify.ts` (see `CONTRIBUTING.md`) |
|
|
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
|
-
| `.github/workflows/release.yml` | The release, each check once:
|
|
81
|
-
| `.github/workflows/weekly.yml` | Weekly, never at release: `verify --full --perf` (perf a report) and the consumer smoke's recovery and failure cases; nothing waits on it |
|
|
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: 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
|
|
@@ -90,11 +90,11 @@ words at the end:
|
|
|
90
90
|
|
|
91
91
|
In Claude Code or Codex, send it as a goal (`/goal Run npx zz-meridian@latest skill --global, then …`): the agent keeps
|
|
92
92
|
working until the condition holds, and a separate model checks it after every turn against the hand-over, which quotes
|
|
93
|
-
its evidence. The sentence says what you want; the skill carries how good it has to be. It builds, renders every view
|
|
94
|
-
|
|
95
|
-
and craft (judged on the renders), and improves it round by round until nothing is weak,
|
|
96
|
-
and recording each decision it made for you. You get the finished product, the renders,
|
|
97
|
-
can answer.
|
|
93
|
+
its evidence. The sentence says what you want; the skill carries how good it has to be. It builds, renders every view at
|
|
94
|
+
phone and desktop width in both themes, scores each one on a scoreboard of floors (measured by
|
|
95
|
+
`pnpm verify --full --timed`) and craft (judged on the renders), and improves it round by round until nothing is weak,
|
|
96
|
+
deciding what it can find out and recording each decision it made for you. You get the finished product, the renders,
|
|
97
|
+
and the few questions only you can answer.
|
|
98
98
|
|
|
99
99
|
You do not choose between the package's commands; the skill does, from what you said and what is in the folder, and
|
|
100
100
|
names the route before it runs anything:
|
|
@@ -108,12 +108,13 @@ names the route before it runs anything:
|
|
|
108
108
|
|
|
109
109
|
`adopt` copies the components, tokens and gates in, merges the dependencies, brands it, adds Meridian's managed block to
|
|
110
110
|
your `AGENTS.md` (your own text there is kept byte for byte) and an empty `docs/brief.md` for your product's own
|
|
111
|
-
context, and installs the skill into the project. `create` writes a new, branded project with the same skill. The
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
and nothing depends on it afterwards; its commands are
|
|
111
|
+
context, and installs the skill into the project. `create` writes a new, branded project with the same skill. The agent
|
|
112
|
+
then does the judgement: building each page on Meridian, and running `pnpm verify` until the project meets the standard
|
|
113
|
+
(`pnpm verify --full` after a change to shared components, the shell or data, and `pnpm verify --full --timed`, the
|
|
114
|
+
whole standard, before it hands over). It needs Node 22.18+, and Google Chrome for the browser checks. If your pages
|
|
115
|
+
call a live API, say so in the sentence: `verify --full` presses every control, Delete included, so the agent builds a
|
|
116
|
+
fake API first. The package (`cli/`, decision 0009) copies files and nothing depends on it afterwards; its commands are
|
|
117
|
+
in `cli/README.md`.
|
|
117
118
|
|
|
118
119
|
## Privacy and feedback
|
|
119
120
|
|
|
@@ -137,7 +138,8 @@ pnpm install
|
|
|
137
138
|
pnpm dev # the template at /, the Design Atlas at /system
|
|
138
139
|
pnpm gate # tokens fresh, registry fresh, specs consistent, contrast in every theme and accent, types, tests
|
|
139
140
|
pnpm verify # the gate once, one production build, the size checks and a smoke of up to three routes
|
|
140
|
-
pnpm verify --full # every route, every control and link, the keyboard walk, the assistant and the
|
|
141
|
+
pnpm verify --full # every route, every control and link, the keyboard walk, the assistant and the product's own checks; no time is judged
|
|
142
|
+
pnpm verify --timed # the navigation budgets, the live data and Web Vitals; needs a quiet machine, runs alone and last
|
|
141
143
|
# (the browser suites look inside open shadow roots; validation.md says what each line means)
|
|
142
144
|
pnpm brand --name "Acme" --hue 25 --chroma 0.16 # rename and rebrand in place
|
|
143
145
|
```
|
|
@@ -74,6 +74,8 @@ Every figure is read on the server by the Home tool's read (`homeTool` in `src/v
|
|
|
74
74
|
|
|
75
75
|
The page's client is handed these figures and lines and works nothing out beyond a change against the period before. Revenue is what customers were billed for metered usage, before credits, and over 30 days it is the customers' revenue on Customers and Revenue to the cent; latency is the median day's p95.
|
|
76
76
|
|
|
77
|
+
The body registers the ten collections its feature needs (`days`, `usage`, `models`, `compute`, `incidents`, `customers`, `services`, `tickets`, `tests` and `releases`) with `useLive` (`src/lib/live.ts`), before the empty state returns, so the page is drawn fresh in every state without a reload. It registers those and no others, because `GET /api/live` answers 403 for any name the caller may not read, and a person who reaches the page may read exactly what its feature needs: `costs`, which the page reads only for a person who may, is not registered. Nothing here writes, so no change hints; the page resyncs on open, on reconnect and on the safety poll.
|
|
78
|
+
|
|
77
79
|
## Surfaces
|
|
78
80
|
|
|
79
81
|
- **Console**: as above.
|
|
@@ -70,7 +70,7 @@ Every figure is read on the server by the Revenue tool's read (`revenueTool` in
|
|
|
70
70
|
- **What they buy** is the `usage` rows' revenue by model over the period's complete days, labelled from `models`; **where they are** is the `regions` collection.
|
|
71
71
|
- **`facts.nextPlanAt`** (`src/app.config.ts`, 0.75 in the sample) is read each time the read runs.
|
|
72
72
|
|
|
73
|
-
Revenue over 30 days equals Home's and the customers' revenue to the cent. Nothing writes.
|
|
73
|
+
Revenue over 30 days equals Home's and the customers' revenue to the cent. Nothing writes. The body registers `customers`, `days`, `usage`, `models` and `regions` with `useLive` (`src/lib/live.ts`), before the empty state returns, so the page is drawn fresh in every state without a reload. Those are the collections its feature needs and no others, because `GET /api/live` answers 403 for any name the caller may not read, and a person who reaches the page may read exactly what its feature needs.
|
|
74
74
|
|
|
75
75
|
## Surfaces
|
|
76
76
|
|
|
@@ -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
|
-
/** `
|
|
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);
|
package/payload/docs/agents.md
CHANGED
|
@@ -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 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
|
|
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
|
|