zz-meridian 0.16.2 → 0.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/README.md +6 -6
  2. package/dist/context.js +2 -1
  3. package/dist/create.js +1 -1
  4. package/dist/migrations.js +34 -2
  5. package/package.json +1 -1
  6. package/payload/AGENTS.md +1 -1
  7. package/payload/CHANGELOG.md +52 -0
  8. package/payload/CONTRIBUTING.md +15 -11
  9. package/payload/README.md +16 -14
  10. package/payload/app/(dashboard)/(home)/README.md +2 -0
  11. package/payload/app/(dashboard)/business/README.md +1 -1
  12. package/payload/app/(dashboard)/members/actions.ts +2 -1
  13. package/payload/app/api/assistant/route.ts +5 -9
  14. package/payload/docs/agents.md +1 -1
  15. package/payload/docs/benchmark.md +1 -1
  16. package/payload/docs/distribution.md +12 -10
  17. package/payload/docs/start-a-dashboard.md +4 -3
  18. package/payload/docs/testing.md +134 -0
  19. package/payload/next.config.ts +3 -0
  20. package/payload/package.json +1 -1
  21. package/payload/scripts/ask.ts +80 -25
  22. package/payload/scripts/brand.ts +1 -1
  23. package/payload/scripts/gate.ts +42 -16
  24. package/payload/scripts/interactions.ts +19 -3
  25. package/payload/scripts/lib/budgets.ts +22 -1
  26. package/payload/scripts/lib/coverage.ts +23 -6
  27. package/payload/scripts/lib/journey.ts +473 -0
  28. package/payload/scripts/lib/routes.ts +37 -10
  29. package/payload/scripts/navigate.ts +13 -469
  30. package/payload/scripts/perf.ts +5 -4
  31. package/payload/scripts/verify.config.ts +1 -1
  32. package/payload/scripts/verify.ts +46 -29
  33. package/payload/skills/zz-meridian/SKILL.md +28 -23
  34. package/payload/skills/zz-meridian/references/agents.md +1 -1
  35. package/payload/skills/zz-meridian/references/customize.md +18 -18
  36. package/payload/skills/zz-meridian/references/existing-project.md +14 -14
  37. package/payload/skills/zz-meridian/references/live.md +3 -1
  38. package/payload/skills/zz-meridian/references/standard.md +25 -23
  39. package/payload/skills/zz-meridian/references/update.md +18 -14
  40. package/payload/skills/zz-meridian/references/validation.md +53 -42
  41. package/payload/src/components/patterns/featured-metric/README.md +5 -2
  42. package/payload/src/components/patterns/featured-metric/index.tsx +34 -11
  43. package/payload/src/components/patterns/featured-metric/preview.tsx +9 -0
  44. package/payload/src/data/metrics.ts +5 -3
  45. package/payload/src/lib/assistant/prompt.ts +15 -3
  46. package/payload/src/lib/assistant/respond.ts +1 -1
  47. package/payload/src/lib/format-date.ts +16 -0
  48. package/payload/src/lib/shared-context.ts +39 -1
  49. package/payload/src/system/card-stage.tsx +2 -2
  50. package/payload/src/views/customer-context.ts +2 -2
  51. package/payload/src/views/customer.tsx +2 -2
  52. package/payload/src/views/home.tsx +3 -0
  53. package/payload/src/views/members.tsx +2 -2
  54. package/payload/src/views/now-parts.ts +5 -5
  55. package/payload/src/views/revenue-parts.ts +2 -2
  56. package/payload/src/views/revenue.tsx +3 -0
  57. package/payload/tests/assistant-prompt-writes.test.ts +69 -0
  58. package/payload/tests/assistant-withheld.test.ts +1 -1
  59. package/payload/tests/featured-metric.test.tsx +72 -0
  60. package/payload/tests/navigate-menu-start.test.ts +1 -1
  61. package/payload/tests/navigate-start.test.ts +1 -1
  62. package/payload/tests/verify-suite-outcome.test.ts +4 -4
  63. package/payload/tests/view-page-match.test.ts +56 -0
  64. package/payload/tests/view-tool-modules.test.ts +50 -0
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
- at phone and desktop width in both themes, scores each one on a scoreboard of floors (measured by `pnpm verify --full`)
18
- and craft (judged on the renders), and improves it round by round until nothing is weak, deciding what it can find out
19
- and recording each decision it made for you. You get the finished product, the renders, and the few questions only you
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\` or \`pnpm verify --perf\` when deep checks are requested; release CI runs both.
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
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
  }
@@ -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 three 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 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.',
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.',
240
240
  checks: ['gate'],
241
241
  },
242
242
  {
@@ -430,6 +430,38 @@ export const RELEASE_MIGRATIONS = [
430
430
  instructions: 'Bring `tests/rail-marker.test.tsx` over from the release as it has it. Since 0.16.2 `src/components/patterns/rail/index.tsx` reads no offset of its own — a rail mounts inside the tap that opens the phone drawer, and a read there lays the page out inside that tap — so the marker\'s first position is the one `observe` delivers, after the effect that called it. A test whose fake answers `observe` with an empty body expects the marker before any delivery and fails; the release\'s fake answers it with the callback, `observe() { for (const cb of observers) queueMicrotask(cb); }`, and reads the marker after that delivery. A test of your own that mounts the rail changes the same way.',
431
431
  checks: ['gate'],
432
432
  },
433
+ {
434
+ id: 'view-page-matched-as-next',
435
+ since: '0.16.3',
436
+ applies: (root) => (/\bfunction onPage\s*\(/.test(text(root, 'app/api/assistant/route.ts') ?? '') ? ['app/api/assistant/route.ts'] : null),
437
+ summary: 'The assistant route finds the view of the page a person is on with `viewAt` from `src/lib/shared-context.ts`, which matches a catch-all `page` as Next.js does.',
438
+ instructions: 'In `app/api/assistant/route.ts`, delete the local `onPage` and find the view with `viewAt` from `@/lib/shared-context`: `const at = viewAt(viewTools, path); const view = at && views.includes(at) ? at : undefined;`, where `views` are the view tools the person may use. `onPage` matched a `[...name]` segment as one segment and a `[[...name]]` never, so a view whose `page` is a catch-all (`/docs/[...path]`) was handed its prepared query at one depth only. `viewAt` matches as Next.js does — `[...name]` one or more segments, `[[...name]]` zero or more — and where two views match one address it takes the one Next.js renders, before asking whether the person may use it, so a less specific view never stands in for the page.',
439
+ checks: ['gate'],
440
+ },
441
+ {
442
+ id: 'journey-moved',
443
+ since: '0.16.3',
444
+ applies: (root) => matching(root, (rel, src) => importsOf(rel, src).some(({ from, names, namespace }) => from === 'scripts/navigate' && (namespace !== null || names.some((n) => n !== 'workspaceHomes')))),
445
+ summary: 'The navigation journey moved from `scripts/navigate.ts` to `scripts/lib/journey.ts`, which keeps every source file Meridian ships under 700 lines.',
446
+ instructions: 'Bring `tests/navigate-start.test.ts` and `tests/navigate-menu-start.test.ts` over from the release as it has them. A file of your own that imports the journey — `startFor`, `journeyStart`, `journey`, `drawerRoutes`, `prepare`, `liveRefresh`, `Fail` and the types `Device`, `Metric`, `Condition`, `Measured`, `Sample` and `NavConfig` — imports it from `scripts/lib/journey.ts` instead; `scripts/navigate.ts` keeps the command and `workspaceHomes` only.',
447
+ checks: ['gate'],
448
+ },
449
+ {
450
+ id: 'today-in-timezone',
451
+ since: '0.16.3',
452
+ applies: (root) => matching(root, (rel, src) => (rel === 'app/api/assistant/route.ts' || rel === 'src/data/metrics.ts') && /\bconst today = clock\(\)\.toISOString\(\)\.slice\(0, 10\)|\btoday: clock\(\)\.toISOString\(\)\.slice\(0, 10\)/.test(src)),
453
+ summary: "Today is the day in `app.timezone` wherever a day is cut: `dayOf(clock())` from `src/lib/format-date.ts`, not the UTC date.",
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
+ checks: ['gate'],
456
+ },
457
+ {
458
+ id: 'verify-tiers',
459
+ since: '0.16.4',
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
+ },
433
465
  ];
434
466
  /**
435
467
  * A test of the rail that stands over a fake `ResizeObserver` answering `observe` with nothing (0.16.2). The lookbehind
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zz-meridian",
3
- "version": "0.16.2",
3
+ "version": "0.17.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.
@@ -2,6 +2,58 @@
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.17.0] · 2026-10-18
6
+
7
+ ### Breaking
8
+
9
+ - **`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.
10
+ - **`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.
11
+ - **`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`.
12
+ - **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`.
13
+ - **`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.
14
+
15
+ ### New
16
+
17
+ - **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.
18
+
19
+ ### Changed
20
+
21
+ - **`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.
22
+
23
+ ### Fixed
24
+
25
+ - **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.
26
+ - **`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.
27
+ - **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.
28
+ - **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.
29
+ - **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`).
30
+ - **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.
31
+ - **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.
32
+ - **`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.
33
+ - **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.
34
+
35
+ ### Known issues
36
+
37
+ - **`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.
38
+ - **`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.
39
+
40
+
41
+ ## [0.16.3] · 2026-10-17
42
+
43
+ ### Breaking
44
+
45
+ - **The navigation journey moved to `scripts/lib/journey.ts`.** A created project's `tests/navigate-start.test.ts` and `tests/navigate-menu-start.test.ts` import `startFor` and `journeyStart` from `scripts/navigate.ts`, which no longer has them, so they fail the type check until they are brought over. The files are the project's own, so the update does not write them: it reports them as the migration `journey-moved` — bring the release's copies over, and import the journey (`journey`, `journeyStart`, `startFor`, `drawerRoutes`, `prepare`, `liveRefresh`, `Fail` and its types) from `scripts/lib/journey.ts` in a file of your own. `scripts/navigate.ts` keeps the command and `workspaceHomes`.
46
+
47
+ ### Changed
48
+
49
+ - **The default verify does less twice, and the gate runs its three long checks side by side.** `scripts/gate.ts` runs the type check, the lint and the tests together once the quick checks before them pass, and prints their lines in the same order as before. All three run to the end, so one failure never hides another. The build inside `pnpm verify` no longer type-checks the tree a second time, because the gate has just checked it with the same tsconfig: verify sets `MERIDIAN_TYPES_CHECKED` for that build, and the template's `next.config.ts` reads it as `typescript.ignoreBuildErrors`. Every other `next build` still checks types. An adopted project keeps its own `next.config`, so its verify checks types twice as before, unless it adds that line. Measured from a clean tree on one machine: the gate 22.9 → 15.4 s, the build 14.5 → 9.5 s, the default verify 62.2 → 49.8 s. The release's verify had reached 193 s against its 180 s limit on the slowest runner CPU.
50
+ - **Every source file Meridian ships is under 700 lines** ([#41](https://github.com/zhixuan312/zz-meridian/issues/41)). `scripts/navigate.ts` had grown to 775, the only managed file past a ceiling a project may hold its own files to — and a project cannot split a managed file, because the update restores it. The journey and its measurements are `scripts/lib/journey.ts` now (467 lines) and `scripts/navigate.ts` keeps the command and its report (319); nothing either does changed. A test of the package holds every shipped file under `src`, `app`, `scripts` and `tests` to 700 lines, so a release cannot grow one past it again.
51
+
52
+ ### Fixed
53
+
54
+ - **Today is the product's day wherever a day is cut** ([#42](https://github.com/zhixuan312/zz-meridian/issues/42)). The assistant's prompt told the model today in `app.timezone`, while the prepared query (`today` in `app/api/assistant/route.ts`) and the pages' periods (`readDays` in `src/data/metrics.ts`) cut their days at the UTC date. So east of UTC after local midnight, and west of it in the evening, the model's today and the end of its `7d` window were a day apart, and a day row — kept under its day in `app.timezone` — fell on the wrong side of the cut. **`dayOf(at)`** (`src/lib/format-date.ts`) is the calendar day of an instant in `app.timezone`, and the route, `readDays` and the sample's views (Now's month and its runs by day, Revenue's ladder and trials, a customer's busiest day and trial days left, a new member's joined date) ask it now. A product whose `app.timezone` is UTC sees no change. The route and `src/data/metrics.ts` are the team's own files, so the update reports them as the migration `today-in-timezone` — replace `clock().toISOString().slice(0, 10)` with `dayOf(clock())`.
55
+ - **A view whose page is a catch-all route is handed its prepared query at every depth** ([#40](https://github.com/zhixuan312/zz-meridian/issues/40)). The assistant route matched a view's `page` against the address segment by segment and required the two to have as many segments, reading `[...name]` as any one segment and `[[...name]]` as one too: a view on `/docs/[...path]` was the page at `/docs/a` and not at `/docs/a/b`, which the same Next.js route renders, and one on `/docs/[[...path]]` was never the page at `/docs`. The match is `viewAt` in `src/lib/shared-context.ts` now, and it matches as Next.js does — `[...name]` one or more segments, `[[...name]]` zero or more — and where two views match one address it takes the one Next.js renders: a static segment over `[name]`, that over a catch-all. The route asks whether the person may use that view after choosing it, so a less specific view never stands in for the page. `app/api/assistant/route.ts` is the team's own file, so the update does not write it: it reports it as the migration `view-page-matched-as-next` — delete the route's `onPage` and find the view with `viewAt`. `node scripts/ask.ts` asks a catch-all page's questions on a link of any depth, and an optional catch-all's on the page with none.
56
+
5
57
  ## [0.16.2] · 2026-10-16
6
58
 
7
59
  ### Breaking
@@ -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 | Before release |
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 changing a component |
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 --full`; `--atlas` after changing what every card page shares |
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 | Before release |
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 | Before release |
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, the live data and Web Vitals | After a change to shared components, the shell or data, and before release |
101
- | `pnpm verify --perf` | The 20-sample navigation protocol; combine with `--full` to run both without repeating the gate or the build | When timing is what changed |
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; 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 |
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 designing |
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 timed default `verify` (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 --perf` runs weekly (`.github/workflows/weekly.yml`), not here.
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.
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: a timed default verify 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 --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: `verify --full --timed --perf` (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
- at phone and desktop width in both themes, scores each one on a scoreboard of floors (measured by `pnpm verify --full`)
95
- and craft (judged on the renders), and improves it round by round until nothing is weak, deciding what it can find out
96
- and recording each decision it made for you. You get the finished product, the renders, and the few questions only you
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
- agent then does the judgement: building each page on Meridian, and running `pnpm verify` until the project meets the
113
- standard (`pnpm verify --full` after a change to shared components, the shell or data). It needs Node 22.18+, and
114
- Google Chrome for the browser checks. If your pages call a live API, say so in the sentence: `verify --full` presses
115
- every control, Delete included, so the agent builds a fake API first. The package (`cli/`, decision 0009) copies files
116
- and nothing depends on it afterwards; its commands are in `cli/README.md`.
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 live data
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
 
@@ -4,6 +4,7 @@ import { clock } from '@/data/collections';
4
4
  import { can, collectionFor, resolveAccess, Unauthenticated } from '@/data/access';
5
5
  import { assignMemberRole, REFUSALS } from '@/data/member-mutations';
6
6
  import type { AnyCollection } from '@/lib/collection';
7
+ import { dayOf } from '@/lib/format-date';
7
8
  import type { Result } from '@/views/members';
8
9
  import { STATUSES, TEAMS, type Member } from '@/data/sample';
9
10
  import { MAIN_ROLES, type AddOn, type MainRole } from '@/data/roles';
@@ -38,7 +39,7 @@ export async function inviteMember(input: { name: string; email: string; role: M
38
39
  if (!MAIN_ROLES.includes(input.role) || !TEAMS.includes(input.team)) return { ok: false, error: 'Choose a role and a team from the lists.' };
39
40
  // The boundary decides the role the caller may give — an Admin inviting an Owner is refused there with the frozen
40
41
  // rule's message — so this path only shapes the invitation.
41
- return run('create', undefined, (c) => c.create!({ name, email, role: input.role, team: input.team, status: 'Invited', joined: clock().toISOString().slice(0, 10), lastActive: null }));
42
+ return run('create', undefined, (c) => c.create!({ name, email, role: input.role, team: input.team, status: 'Invited', joined: dayOf(clock()), lastActive: null }));
42
43
  }
43
44
 
44
45
  export async function setMemberStatus(id: string, status: Member['status']): Promise<Result> {
@@ -12,7 +12,8 @@ import { handedTo } from '@/lib/assistant/scoped';
12
12
  import type { AgentChange, HandedExplore } from '@/lib/assistant/tools';
13
13
  import type { AnyCollection, Grant, Need } from '@/lib/collection';
14
14
  import type { PreparedQuery } from '@/lib/explore';
15
- import type { ViewTool } from '@/lib/shared-context';
15
+ import { dayOf } from '@/lib/format-date';
16
+ import { viewAt, type ViewTool } from '@/lib/shared-context';
16
17
  import { viewTools } from '@/views/tools';
17
18
 
18
19
  /** The panel sends at most the last 100 messages; a larger body than 2 MB is refused before it is parsed. */
@@ -62,14 +63,15 @@ export async function POST(request: Request): Promise<Response> {
62
63
  // they may use it. It reads through `aggregate()` and `read()`, which ask the person's grant again when a call runs;
63
64
  // its schema offers only what they may read: the collections handed above, without each one's hidden fields.
64
65
  const path = pathOf(String(page?.path ?? ''));
65
- const view = views.find((v) => onPage(v.page, path));
66
+ const at = viewAt(viewTools, path);
67
+ const view = at && views.includes(at) ? at : undefined;
66
68
  const names = new Set(readable.map((c) => c.name));
67
69
  const reads = new Set(view ? Object.values(view.explore.sources).map((s) => s.collection) : []);
68
70
  const explore: HandedExplore | undefined = view && [...reads].some((name) => names.has(name))
69
71
  ? {
70
72
  view,
71
73
  rights: { readable: names, hidden: (name) => (readable.find((c) => c.name === name)?.hidden ?? []) as string[] },
72
- io: { aggregate, read, today: clock().toISOString().slice(0, 10) },
74
+ io: { aggregate, read, today: dayOf(clock()) },
73
75
  page: path,
74
76
  }
75
77
  : undefined;
@@ -145,12 +147,6 @@ function pathOf(address: string): string {
145
147
  return (path.length > 1 ? path.replace(/\/+$/, '') : path) || '/';
146
148
  }
147
149
 
148
- /** Whether `path` is the page `route` renders: each segment equal, a `[name]` segment matching any one segment. */
149
- function onPage(route: string, path: string): boolean {
150
- const want = route.split('/'), got = path.split('/');
151
- return want.length === got.length && want.every((w, i) => (/^\[[^\]/]+\]$/.test(w) ? got[i] !== '' : w === got[i]));
152
- }
153
-
154
150
  /** The grants a need names: the one, every one of an `allOf`, and none for `public`. */
155
151
  const grantsOf = (need: Need): readonly Grant[] => (need === 'public' ? [] : typeof need === 'string' ? [need] : need.allOf);
156
152
 
@@ -83,7 +83,7 @@ The answer is text of at most 3,000 characters, in a fixed order: the window as
83
83
 
84
84
  ## The console's own assistant
85
85
 
86
- The second place is inside the product. The assistant panel reads the page the person is on (its address with the query, the view's shared context, then the visible text), reads further through every `view_<name>` and the page's own prepared query, and proposes changes as Proposals in its thread. It is handed that page's `explore_<view>` and no other page's: the route finds the view whose `page`, the console route of the page it renders, is the address the panel sends, and hands its prepared query only where the person may use that view. There is no tool that reads a collection directly. Its write tools are those of the collections that prepared query reads, among the product's collections (`src/data/collections.ts`), so the members' writes are handed on Members and the key writes on API keys; a change takes the ids the query's drilled rows give, and asked for a change another page makes, the assistant gives that page's address where the person may open it; a page they may not open is never offered as where they make a change. A rail page's `page` is its `href` in `nav`; a detail page's has its segment (`/customers/[id]`) and matches any one segment there. It follows the same rules: it reads what the person may read, changes nothing without approval, and leaves its mark (the Agent mark and the "Assistant" caption). It is off until a model is configured. `docs/assistant.md` says how to switch it on and point it at your data, and how the same tools can later be offered from an MCP server.
86
+ The second place is inside the product. The assistant panel reads the page the person is on (its address with the query, the view's shared context, then the visible text), reads further through every `view_<name>` and the page's own prepared query, and proposes changes as Proposals in its thread. It is handed that page's `explore_<view>` and no other page's: the route finds the view whose `page`, the console route of the page it renders, is the address the panel sends, and hands its prepared query only where the person may use that view. There is no tool that reads a collection directly. Its write tools are those of the collections that prepared query reads, among the product's collections (`src/data/collections.ts`), so the members' writes are handed on Members and the key writes on API keys; a change takes the ids the query's drilled rows give, and asked for a change another page makes, the assistant gives that page's address where the person may open it; a page they may not open is never offered as where they make a change. A rail page's `page` is its `href` in `nav`; a detail page's has its segment (`/customers/[id]`) and matches any one segment there, and a catch-all matches as Next.js matches it: `/docs/[...path]` any depth under `/docs/`, `/docs/[[...path]]` `/docs` as well. Where two views match one address, the one Next.js renders is chosen (`viewAt` in `src/lib/shared-context.ts`). It follows the same rules: it reads what the person may read, changes nothing without approval, and leaves its mark (the Agent mark and the "Assistant" caption). It is off until a model is configured. `docs/assistant.md` says how to switch it on and point it at your data, and how the same tools can later be offered from an MCP server.
87
87
 
88
88
  The page text it reads is the scroll region's visible text, less every node marked `data-assistant="omit"`. A value shown once, such as a newly issued key's secret or a one-time sign-up link, is drawn so marked, or outside the scroll region (a `Dialog` portals to the document's body), so asking while it is on screen never sends it to the model; the sample's API keys page marks a new key's Copy field so. `scripts/ask.ts` leaves out the same nodes from the page it sends. The thread is the signed-in person's for the tenant they act for: it is stored with that scope, a change of either starts an empty thread and removes the earlier one, and each request names the scope, so a session that changed hands since (a sign-in from another tab) is refused with 409 and the panel clears the thread instead of continuing it, then refreshes the route, so the next question is asked as whoever is signed in now. A tab writes and removes only a thread of its own scope.
89
89
 
@@ -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 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 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.
114
114
 
115
115
  ## Open
116
116
 
@@ -18,10 +18,10 @@ An earlier sentence named `adopt`, the wrong command for every request for a new
18
18
  The sentence is written to be sent as a goal (`/goal` in Claude Code and Codex), which keeps the agent working until a
19
19
  separate model, reading the conversation after every turn, finds the condition met. That model sees the sentence, not
20
20
  the skill, so the sentence ends with a condition the transcript can show: the hand-over (SKILL.md, step 7) quotes
21
- `pnpm verify --full`'s last line and the scoreboard's count, or names what only the person can decide or provide. The
22
- degree lives in the skill, never in the sentence: `skills/zz-meridian/references/standard.md` holds the register, the
23
- scoreboard of floors and craft kept for every view, the loop run until nothing is weak, and which decisions the agent
24
- makes and records and which wait for the person. People do not say how good it has to be, so the skill does not ask.
21
+ `pnpm verify --full --timed`'s last line and the scoreboard's count, or names what only the person can decide or
22
+ provide. The degree lives in the skill, never in the sentence: `skills/zz-meridian/references/standard.md` holds the
23
+ register, the scoreboard of floors and craft kept for every view, the loop run until nothing is weak, and which
24
+ decisions the agent makes and records and which wait for the person. People do not say how good it has to be, so the skill does not ask.
25
25
 
26
26
  The split follows what each part is good at. The package does the settled work, the same way every time, and proves it
27
27
  built: what to copy, which dependencies to merge, the alias, the stylesheet, the brand. The agent does the judgement:
@@ -147,14 +147,14 @@ Two environment variables exist for the updater's own tests and are used only wh
147
147
  reports as `migration:<id>` because it cannot make them. An entry applies when the project's recorded version is older
148
148
  than its `since` and the target is not, and its `applies(root)` returns the paths that still have the old shape, or
149
149
  null. It only reads, and only team-owned files (the ownership rule's managed paths are skipped); a file it cannot read is
150
- absent. A project already on the new shape gets no line. Every entry carries `gate` and `build` as its checks (`verify-modes`, `agent-reads-section`, `nav-needs`, `agent-reads-own-group` and `marker-measured-on-observe`, `gate` alone), and its
150
+ absent. A project already on the new shape gets no line. Every entry carries `gate` and `build` as its checks (`verify-modes`, `agent-reads-section`, `nav-needs`, `agent-reads-own-group`, `agent-does-here-section`, `marker-measured-on-observe`, `view-page-matched-as-next`, `journey-moved`, `today-in-timezone` and `verify-tiers`, `gate` alone), and its
151
151
  instructions name the section of a reference (`cache.md`, `live.md`, `validation.md`, `agents.md`, `customize.md`), of the Start guide or of `docs/assistant.md` to follow.
152
152
 
153
- Eleven 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), and `marker-measured-on-observe` (0.16.2). The ten of 0.5.0 are:
153
+ Fifteen arrived after 0.5.0 and are resolved as the skill's `references/update.md` says: `share-view-context` and `agent-reads-section` (0.9.0), `assistant-view-tools` (0.10.0), `nav-needs` (0.13.0), `agent-reads-own-group` (0.15.0), `refusal-declared`, `managed-exports-removed`, `view-tool-page-explore`, `agent-does-here-section` and `sample-pages-renamed` (0.16.0), `marker-measured-on-observe` (0.16.2), `view-page-matched-as-next`, `journey-moved` and `today-in-timezone` (0.16.3), and `verify-tiers` (0.16.4). The ten of 0.5.0 are:
154
154
 
155
155
  | Id | Reported when a team-owned file still has | Completion step |
156
156
  |----|-------------------------------------------|-----------------|
157
- | `verify-modes` | a `package.json` script, workflow or shell file passing `--quick`, `--no-vitals` or `--extra` to `verify` | `validation.md`: replace each with `pnpm verify`, `--full` or `--perf` in the script, workflow or shell file |
157
+ | `verify-modes` | a `package.json` script, workflow or shell file passing `--quick`, `--no-vitals` or `--extra` to `verify` | `validation.md`: replace each with `pnpm verify`, `--full`, `--timed` or `--perf` in the script, workflow or shell file |
158
158
  | `shell-assistant-promise` | `<AppShell` with an `assistant=` prop and no promise (`.then(`, `Promise`, `use(`) | `cache.md`, "The shell's assistant promise": pass a promise and do not await it |
159
159
  | `assistant-available-promise` | `useAssistantAvailable()` used as a boolean | same section: `use(useAssistantAvailable())` inside `Suspense` |
160
160
  | `clock-now-required` | `<Freshness`, `<ShellTools` or `<AlertsPanel` without `now=`, or `formatRelative(` with one argument | `cache.md`, "The clock is the data's": pass the data's clock |
@@ -216,12 +216,13 @@ Modelled on the release pipeline of the owner's earlier packages, one package in
216
216
  1. **Dispatch**: `gh workflow run release.yml -f version=<v> [-f dry_run=true]`, from `master`. The version must equal
217
217
  `cli/package.json`'s and the tag must be unused.
218
218
  2. **Gates** (ubuntu-24.04, 4 CPUs, the reference profile), each run once: the package's own tests (`tests/cli-*`) and the
219
- browser-fixture tests of the checks (`tests/deep-*`), which no adopted or created project has and the timed default
219
+ browser-fixture tests of the checks (`tests/deep-*`), which no adopted or created project has and the default
220
220
  `verify` leaves out; the template's default `verify` from a clean `.next` through
221
221
  `cli/scripts/release-check.ts`, within 180 s on the shared runner (whose CPU varies: one commit measured 111 s on one
222
222
  and 152 s on another) and with its browser smoke (the gate and the template's unit tests run
223
223
  inside it); and the consumer smoke from the built tarball (step 4). The full, perf and recovery suites are never
224
224
  run here: they run weekly (below).
225
+ When each check runs, in a project and in this repository, is `docs/testing.md`'s `## When each runs`.
225
226
  3. **Pack and assert, before anything is irreversible**: `npm pack` in `cli/`, then on the tarball: the bin has its
226
227
  `#!/usr/bin/env node` line; `payload/skills/zz-meridian/SKILL.md` and its references are there; at least 60 component
227
228
  folders are there; no `node_modules/`, `tests/` of the CLI, `out/` or `.next/`.
@@ -246,14 +247,15 @@ Modelled on the release pipeline of the owner's earlier packages, one package in
246
247
  `dry_run` stops after step 4.
247
248
 
248
249
  **Weekly** (`.github/workflows/weekly.yml`, Mondays and on demand, master's current commit): the template's
249
- `verify --full --perf`, one gate and one build for both (the perf part a report: a p95 over budget is a warning, a
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
250
251
  broken sample a failure), and the consumer smoke's `--adopt --update --origins 3 --update-all --verify` from a tarball packed from that
251
252
  commit, labelled as the next patch so `update` treats it as unpublished: the adopted project's default verify cases,
252
253
  the update from each of the last three releases, the updater's stages on the 0.3.0 fixture, and every recovery and
253
254
  failure case of the update. Both run the
254
255
  consumer smoke's `--adopt` and `--update`; weekly reaches three releases back and every recovery case, a release only the
255
256
  one before it. Each job keeps its log as an artifact. A failure notifies;
256
- nothing waits on it.
257
+ nothing waits on it. `docs/testing.md` says when each check runs, and that a red here does not of itself hold
258
+ a release.
257
259
 
258
260
  A `/release` runbook (`.claude/commands/release.md`) holds the judgement before dispatch: the version, the changelog
259
261
  section and the docs sweep. A `cli/scripts/set-version.ts`
@@ -44,7 +44,7 @@ Your pages, your server actions and the assistant read and change records throug
44
44
  - Read freshness from when the data arrived (the newest ingest time), never from `now()`.
45
45
  - Return `null` for "not measured"; the formatters render it as a dash, never as zero.
46
46
 
47
- The console keeps its collection-backed pages (Now, Members, API keys, Tickets, Tests, Releases) fresh without a reload: one stream per tab carries a hint that names only a collection, and the page refreshes through the same authorized `read()`. The sample stream is a single process; across processes, `skills/zz-meridian/references/live.md` holds the Postgres and Redis adapters a product writes for itself.
47
+ The console keeps its collection-backed pages (Home, Revenue, Now, Members, API keys, Tickets, Tests, Releases) fresh without a reload: one stream per tab carries a hint that names only a collection, and the page refreshes through the same authorized `read()`. The sample stream is a single process; across processes, `skills/zz-meridian/references/live.md` holds the Postgres and Redis adapters a product writes for itself.
48
48
 
49
49
  ## 5. Decide who may do what
50
50
 
@@ -91,7 +91,8 @@ The console's assistant is off until you set the `ASSISTANT_*` variables (`.env.
91
91
 
92
92
  ```sh
93
93
  pnpm verify # the gate once, one production build, the size checks and a smoke of up to three routes
94
- pnpm verify --full # every route, every control pressed and link followed, the keyboard walk, the assistant and the live data
94
+ pnpm verify --full # every route, every control pressed and link followed, the keyboard walk, the assistant and the product's own checks; no time is judged
95
+ pnpm verify --timed # the navigation budgets, the live data and Web Vitals; needs a quiet machine, runs alone and last
95
96
  ```
96
97
 
97
- `pnpm verify` is the check to run before you finish. Run `--full` after a change to shared components, the shell or data. 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, 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` 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` 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.