zz-meridian 0.17.0 → 0.18.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (93) hide show
  1. package/dist/adopt.js +3 -3
  2. package/dist/context.js +1 -1
  3. package/dist/files.js +5 -0
  4. package/dist/migrations.js +9 -9
  5. package/dist/reconcile.js +1 -1
  6. package/dist/session.js +2 -2
  7. package/package.json +1 -1
  8. package/payload/CHANGELOG.md +55 -0
  9. package/payload/CONTRIBUTING.md +2 -2
  10. package/payload/README.md +1 -1
  11. package/payload/app/(dashboard)/engineering/page.tsx +1 -1
  12. package/payload/app/(dashboard)/keys/page.tsx +2 -1
  13. package/payload/app/(dashboard)/members/page.tsx +2 -2
  14. package/payload/app/(dashboard)/operations-tickets/page.tsx +2 -1
  15. package/payload/app/(dashboard)/releases/page.tsx +2 -2
  16. package/payload/app/(dashboard)/tests/page.tsx +2 -2
  17. package/payload/app/api/live/route.ts +19 -1
  18. package/payload/docs/agents.md +2 -0
  19. package/payload/docs/benchmark.md +1 -1
  20. package/payload/docs/distribution.md +6 -5
  21. package/payload/docs/skill-evals.md +1 -1
  22. package/payload/docs/start-a-dashboard.md +1 -1
  23. package/payload/docs/testing.md +36 -27
  24. package/payload/journeys/workspaces.ts +5 -1
  25. package/payload/package.json +17 -16
  26. package/payload/pnpm-lock.yaml +918 -892
  27. package/payload/pnpm-workspace.yaml +0 -2
  28. package/payload/scripts/audit.ts +7 -0
  29. package/payload/scripts/fake-llm.ts +22 -11
  30. package/payload/scripts/interactions.ts +9 -4
  31. package/payload/scripts/lib/chrome.ts +5 -0
  32. package/payload/scripts/lib/config-hooks.ts +17 -0
  33. package/payload/scripts/lib/coverage.ts +2 -2
  34. package/payload/scripts/lib/frame-cause.ts +41 -0
  35. package/payload/scripts/lib/journey.ts +11 -5
  36. package/payload/scripts/lib/links.ts +14 -0
  37. package/payload/scripts/lib/phone.ts +54 -0
  38. package/payload/scripts/lib/shard.ts +23 -0
  39. package/payload/scripts/lib/sizes.ts +8 -2
  40. package/payload/scripts/live.ts +6 -6
  41. package/payload/scripts/navigate.ts +1 -1
  42. package/payload/scripts/perf.ts +15 -4
  43. package/payload/scripts/shot.ts +22 -4
  44. package/payload/scripts/verify.baseline.json +29 -29
  45. package/payload/scripts/verify.config.ts +14 -6
  46. package/payload/scripts/verify.ts +52 -7
  47. package/payload/scripts/vitals.ts +32 -21
  48. package/payload/skills/zz-meridian/references/cache.md +13 -8
  49. package/payload/skills/zz-meridian/references/existing-project.md +5 -1
  50. package/payload/skills/zz-meridian/references/live.md +53 -8
  51. package/payload/skills/zz-meridian/references/standard.md +1 -1
  52. package/payload/skills/zz-meridian/references/update.md +2 -2
  53. package/payload/skills/zz-meridian/references/validation.md +22 -16
  54. package/payload/src/app.config.ts +6 -2
  55. package/payload/src/components/base/shell/README.md +1 -1
  56. package/payload/src/components/base/shell/index.tsx +25 -12
  57. package/payload/src/components/base/use-nav-drawer.ts +31 -3
  58. package/payload/src/components/patterns/metric-tile/README.md +1 -1
  59. package/payload/src/components/patterns/metric-tile/index.tsx +2 -1
  60. package/payload/src/components/patterns/rail/README.md +2 -2
  61. package/payload/src/components/patterns/rail/index.tsx +6 -0
  62. package/payload/src/components/ui/segmented/README.md +2 -0
  63. package/payload/src/components/ui/segmented/index.tsx +8 -7
  64. package/payload/src/data/README.md +1 -1
  65. package/payload/src/data/engineering.ts +10 -5
  66. package/payload/src/data/live-stream.ts +12 -4
  67. package/payload/src/data/metrics.ts +38 -1
  68. package/payload/src/data/read.ts +13 -8
  69. package/payload/src/data/tickets.ts +4 -4
  70. package/payload/src/lib/collection.ts +18 -3
  71. package/payload/src/lib/live.ts +91 -22
  72. package/payload/src/styles/base.css +12 -0
  73. package/payload/src/system/atlas-shell.tsx +7 -6
  74. package/payload/src/system/page-stage.tsx +7 -2
  75. package/payload/src/views/home.tsx +3 -3
  76. package/payload/src/views/keys.tsx +3 -3
  77. package/payload/src/views/members.tsx +3 -3
  78. package/payload/src/views/now.tsx +5 -5
  79. package/payload/src/views/releases.tsx +3 -3
  80. package/payload/src/views/revenue.tsx +3 -3
  81. package/payload/src/views/services-context.ts +10 -1
  82. package/payload/src/views/services.tsx +2 -3
  83. package/payload/src/views/tests.tsx +3 -3
  84. package/payload/src/views/tickets.tsx +4 -3
  85. package/payload/src/views/tools.ts +11 -11
  86. package/payload/tests/explore-runner.test.ts +6 -4
  87. package/payload/tests/live-client.test.tsx +114 -0
  88. package/payload/tests/live-resume.test.ts +65 -0
  89. package/payload/tests/live-revision-boot.test.ts +20 -0
  90. package/payload/tests/metric-tile-title.test.tsx +14 -0
  91. package/payload/tests/segmented-measure.test.tsx +40 -0
  92. package/payload/tests/shell-drawer-open.test.tsx +49 -0
  93. package/payload/tests/workspace-data-paths.test.ts +1 -1
package/dist/adopt.js CHANGED
@@ -8,12 +8,12 @@
8
8
  */
9
9
  import fs from 'node:fs';
10
10
  import path from 'node:path';
11
- import { VERSION, brandArgs, effectiveBrand, inside, installSkill, jsoncExtras, packageManager, payloadBytes, payloadFiles, readJsonc, readPayload, relativeImports, run, runTool, sha256, writeIn, writeManifest, } from './files.js';
11
+ import { VERSION, brandArgs, effectiveBrand, inside, installArgs, installSkill, jsoncExtras, packageManager, payloadBytes, payloadFiles, readJsonc, readPayload, relativeImports, run, runTool, sha256, writeIn, writeManifest, } from './files.js';
12
12
  import { BRIEF_TEMPLATE, managedBlock, upsertManagedBlock } from './context.js';
13
13
  import { FIXED, adoptSetOf, managedPaths } from './ownership.js';
14
14
  /** The template's dependencies a dashboard built from these files needs. */
15
15
  const DEPS = ['next', 'react', 'react-dom', 'radix-ui', 'lucide-react', 'clsx', 'tailwind-merge', 'react-markdown', 'remark-gfm', 'ai', '@ai-sdk/react', 'zod'];
16
- const DEV_DEPS = ['typescript', '@types/node', '@types/react', '@types/react-dom', 'tailwindcss', '@tailwindcss/postcss', 'eslint', 'eslint-config-next', 'vitest', '@vitejs/plugin-react', 'jsdom', '@testing-library/react', '@testing-library/jest-dom'];
16
+ const DEV_DEPS = ['typescript', '@types/node', '@types/react', '@types/react-dom', 'tailwindcss', '@tailwindcss/postcss', 'eslint', 'eslint-config-next', 'vitest', '@vitejs/plugin-react', 'jsdom', '@testing-library/react', '@testing-library/dom', '@testing-library/jest-dom'];
17
17
  const SCRIPTS = ['typecheck', 'test', 'tokens', 'check', 'contrast', 'gate', 'audit', 'verify', 'brand', 'shot', 'interactions', 'keyboard', 'vitals'];
18
18
  const major = (spec) => Number(/(\d+)/.exec(spec)?.[1] ?? NaN);
19
19
  const TS = /\.(ts|tsx)$/;
@@ -356,7 +356,7 @@ Rename or move them, then run adopt again.`);
356
356
  let typed = 'not run (--no-install)';
357
357
  if (o.install) {
358
358
  say(`\nInstalling with ${pm}…`);
359
- if (run(pm, ['install'], root).status !== 0)
359
+ if (run(pm, installArgs(pm), root).status !== 0)
360
360
  return fail(`${pm} install failed (above). Fix it, then run ${pm} install and ${pm} run typecheck.`);
361
361
  // Route types first (LayoutProps, PageProps), as the gate does.
362
362
  runTool(root, 'next/dist/bin/next', ['typegen']);
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. */
package/dist/files.js CHANGED
@@ -77,6 +77,11 @@ export function packageManager(root) {
77
77
  return 'bun';
78
78
  return 'npm';
79
79
  }
80
+ /**
81
+ * The install that follows Meridian's own edit to package.json. pnpm freezes the lockfile where `CI` is set and refuses
82
+ * the edit it was run to take in, so it is asked to write the lockfile; the other managers write it as they are.
83
+ */
84
+ export const installArgs = (pm) => (pm === 'pnpm' ? ['install', '--no-frozen-lockfile'] : ['install']);
80
85
  /** A tool in the project's node_modules, run as `node <its script>`: no shell and no .cmd shim on any platform. */
81
86
  export function runTool(root, entry, args, quiet = true) {
82
87
  return spawnSync(process.execPath, [path.join(root, 'node_modules', entry), ...args], { cwd: root, encoding: 'utf8', stdio: quiet ? 'pipe' : 'inherit' });
@@ -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/dist/reconcile.js CHANGED
@@ -3,7 +3,7 @@
3
3
  import { builtinModules } from 'node:module';
4
4
  import { upsertManagedBlock } from './context.js';
5
5
  /** Adopt's dev dependencies and scripts, which `update` also needs. They mirror `DEV_DEPS` and `SCRIPTS` in adopt.ts. */
6
- export const TOOLCHAIN = ['typescript', '@types/node', '@types/react', '@types/react-dom', 'tailwindcss', '@tailwindcss/postcss', 'eslint', 'eslint-config-next', 'vitest', '@vitejs/plugin-react', 'jsdom', '@testing-library/react', '@testing-library/jest-dom'];
6
+ export const TOOLCHAIN = ['typescript', '@types/node', '@types/react', '@types/react-dom', 'tailwindcss', '@tailwindcss/postcss', 'eslint', 'eslint-config-next', 'vitest', '@vitejs/plugin-react', 'jsdom', '@testing-library/react', '@testing-library/dom', '@testing-library/jest-dom'];
7
7
  export const MERIDIAN_SCRIPTS = ['typecheck', 'test', 'tokens', 'check', 'contrast', 'gate', 'audit', 'verify', 'brand', 'shot', 'interactions', 'keyboard', 'vitals'];
8
8
  const isObj = (v) => typeof v === 'object' && v !== null && !Array.isArray(v);
9
9
  /**
package/dist/session.js CHANGED
@@ -7,7 +7,7 @@
7
7
  import fs from 'node:fs';
8
8
  import path from 'node:path';
9
9
  import { managedBlock } from './context.js';
10
- import { inside, packageManager, run as spawn, sha256 } from './files.js';
10
+ import { inside, installArgs, packageManager, run as spawn, sha256 } from './files.js';
11
11
  import { aliasesOf, canonicalImports } from './imports.js';
12
12
  import { changedPaths, takeInventory } from './inventory.js';
13
13
  import { renderMergeReport } from './merge-report.js';
@@ -273,7 +273,7 @@ function install(ctx, session, j, pm) {
273
273
  if (before.exists && !fs.existsSync(backup))
274
274
  writeAtomic(backup, fs.readFileSync(inside(root, lock)));
275
275
  const t0 = performance.now();
276
- const r = ctx.run(pm, ['install'], root);
276
+ const r = ctx.run(pm, installArgs(pm), root);
277
277
  const ms = performance.now() - t0;
278
278
  writeAtomic(path.join(session, 'evidence', '0-install.log'), r.output);
279
279
  j.validation = [{ command: `${pm} install`, exitCode: r.status, evidenceFile: 'evidence/0-install.log' }, ...j.validation.filter((v) => !isInstall(v))];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zz-meridian",
3
- "version": "0.17.0",
3
+ "version": "0.18.1",
4
4
  "description": "Bring ZZ Meridian, a dashboard design system, into a Next.js project, or start a new dashboard on it. Copies the files in; nothing depends on this package at runtime.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -2,6 +2,61 @@
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.1] · 2026-10-19
6
+
7
+ ### Changed
8
+
9
+ - **Next.js 16.4.2**, from 16.3.8. 16.4.1 carries fixes for two critical advisories, remote code execution through a `use cache` function that returns a typed array (GHSA-673q-mjm6-r5xr) and through the Image Optimization API with SVG files (GHSA-2464-fhr4-w9xw), and a high one, denial of service through Server Actions (GHSA-p3m5-59mp-jv5r); 16.3 has no release that carries them. `npx zz-meridian@latest update` raises `next` and `eslint-config-next` to `16.4.2` in a project's `package.json`; run your package manager's install after it.
10
+ - **The other dependencies, within their majors**: React 19.3.1, `ai` 7.0.143 with `@ai-sdk/react` 4.0.147, `@ai-sdk/anthropic` 4.0.82 and `@ai-sdk/openai-compatible` 3.0.71, `radix-ui` 1.8.1, `lucide-react` 1.59.0, `zod` 4.6.7, and for development `vitest` 5.0.5, `jsdom` 30.1.3 and `@vitejs/plugin-react` 6.1.2.
11
+ - **`@testing-library/jest-dom` 7**, which needs `@testing-library/dom` as a dependency of the project's own. `adopt` and `update` add `@testing-library/dom` `^10.4.2` to `devDependencies`; `update` raises jest-dom to 7 where Meridian wrote it and asks you to where you chose your own. The matchers are unchanged.
12
+ - TypeScript stays at 5.9 and ESLint at 9: `typescript-eslint` supports TypeScript below 6.1, and `eslint-plugin-react`, `eslint-plugin-import` and `eslint-plugin-jsx-a11y`, which `eslint-config-next` installs, do not yet declare ESLint 10.
13
+
14
+ ### Fixed
15
+
16
+ - **`update` and `adopt` install where `CI` is set.** Both change `package.json` and then run the project's install, and pnpm freezes the lockfile where `CI` is set, so an update that raised a dependency failed with `ERR_PNPM_OUTDATED_LOCKFILE` in CI or under an agent that sets it. With pnpm they now run `pnpm install --no-frozen-lockfile`; npm, yarn and bun run `install` as before.
17
+ - **A client navigation no longer reads the new page's collections again** ([#54](https://github.com/zhixuan312/zz-meridian/issues/54)). 0.18.0 sent the revisions a page was served only on the tab's first stream, so a press in the rail to a page that follows other collections reopened the stream without them, and every collection that page follows was read by its server and again by the stream's resync. `src/lib/live.ts` now sends them on every open that follows a change of the names, and only a reopen after a gap (an error, a tab shown again, the poll's retry) resyncs every name. A created project takes the fix with `npx zz-meridian@latest update`.
18
+
19
+ ## [0.18.0] · 2026-10-19
20
+
21
+ ### Breaking
22
+
23
+ - **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.
24
+ - **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.
25
+ - **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.
26
+
27
+ ### New
28
+
29
+ - **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`.
30
+ - **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.
31
+ - **`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.
32
+
33
+ ### Changed
34
+
35
+ - **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.
36
+ - **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.
37
+ - **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.
38
+ - **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)`.
39
+
40
+ ### Fixed
41
+
42
+ - **`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'`.
43
+ - **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.
44
+ - **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.
45
+ - **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.
46
+ - **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.
47
+ - **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.
48
+ - **`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.
49
+ - **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>`.
50
+ - **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.
51
+ - **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.
52
+ - **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.
53
+ - **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/`.
54
+ - **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.
55
+
56
+ ### Known issues
57
+
58
+ - **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.
59
+
5
60
  ## [0.17.0] · 2026-10-18
6
61
 
7
62
  ### 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
 
@@ -75,7 +75,7 @@ Then, in this order:
75
75
  the accent and the contrast gate. Then it writes Meridian's managed block into their `AGENTS.md`, and `docs/brief.md`
76
76
  when they have none.
77
77
  5. Installs the skill and writes the manifest.
78
- 6. Runs their package manager's install, `next typegen` and `tsc --noEmit`, and prints the result and the next step:
78
+ 6. Runs their package manager's install (pnpm with `--no-frozen-lockfile`, since adopt has just changed `package.json`), `next typegen` and `tsc --noEmit`, and prints the result and the next step:
79
79
  the skill's Route A from step 2, with the template to read from at the same version.
80
80
 
81
81
  Three things make the copy safe in someone else's project. Meridian's own files import each other by relative path,
@@ -107,7 +107,8 @@ three edits named below. It needs a project that adopted or created Meridian 0.3
107
107
  2. **Update.** `npx zz-meridian@latest update` from a clean git tree (or with `--allow-dirty`). It writes the plan first
108
108
  (`.meridian/update/<version>/state.json`, the journal, `MERGE.md`, an empty `resolutions.json`, the `base/`, `ours/`
109
109
  and `new/` copies of every staged file, and a `backup/` preimage of every path it changes), then applies the safe
110
- changes, then runs the install. Applied: untouched replacements, additions and untouched removals. Staged: every
110
+ changes, then runs the install (pnpm with `--no-frozen-lockfile`, which CI would otherwise refuse after the edit to
111
+ `package.json`). Applied: untouched replacements, additions and untouched removals. Staged: every
111
112
  managed file the team changed or deleted, or collided with. The only team files it edits are `package.json` (missing
112
113
  dependencies and scripts, older ones of the same major, and entries still exactly as the earlier release wrote them;
113
114
  the framework `next`, `react` and `react-dom` is always among them), the managed block in `AGENTS.md`, and the
@@ -150,7 +151,7 @@ null. It only reads, and only team-owned files (the ownership rule's managed pat
150
151
  absent. A project already on the new shape gets no line. Every entry carries `gate` and `build` as its checks (`verify-modes`, `agent-reads-section`, `nav-needs`, `agent-reads-own-group`, `agent-does-here-section`, `marker-measured-on-observe`, `view-page-matched-as-next`, `journey-moved`, `today-in-timezone` and `verify-tiers`, `gate` alone), and its
151
152
  instructions name the section of a reference (`cache.md`, `live.md`, `validation.md`, `agents.md`, `customize.md`), of the Start guide or of `docs/assistant.md` to follow.
152
153
 
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:
154
+ 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
155
 
155
156
  | Id | Reported when a team-owned file still has | Completion step |
156
157
  |----|-------------------------------------------|-----------------|
@@ -247,8 +248,8 @@ Modelled on the release pipeline of the owner's earlier packages, one package in
247
248
  `dry_run` stops after step 4.
248
249
 
249
250
  **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
251
+ 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
252
+ 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
253
  commit, labelled as the next patch so `update` treats it as unpublished: the adopted project's default verify cases,
253
254
  the update from each of the last three releases, the updater's stages on the 0.3.0 fixture, and every recovery and
254
255
  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.