zz-meridian 0.27.1 → 0.29.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 (220) hide show
  1. package/README.md +5 -0
  2. package/dist/migrations.js +62 -0
  3. package/dist/ownership.js +30 -5
  4. package/dist/session.js +9 -1
  5. package/package.json +1 -1
  6. package/payload/CHANGELOG.md +36 -0
  7. package/payload/README.md +2 -0
  8. package/payload/app/(dashboard)/(home)/README.md +8 -8
  9. package/payload/app/(dashboard)/(home)/page.tsx +5 -4
  10. package/payload/app/(dashboard)/business/README.md +5 -5
  11. package/payload/app/(dashboard)/business/page.tsx +5 -4
  12. package/payload/app/(dashboard)/customers/README.md +3 -3
  13. package/payload/app/(dashboard)/customers/[id]/README.md +4 -4
  14. package/payload/app/(dashboard)/customers/[id]/page.tsx +3 -2
  15. package/payload/app/(dashboard)/customers/page.tsx +3 -2
  16. package/payload/app/(dashboard)/engineering/README.md +9 -9
  17. package/payload/app/(dashboard)/engineering/page.tsx +5 -4
  18. package/payload/app/(dashboard)/keys/README.md +2 -2
  19. package/payload/app/(dashboard)/keys/actions.ts +4 -4
  20. package/payload/app/(dashboard)/keys/page.tsx +4 -3
  21. package/payload/app/(dashboard)/layout.tsx +1 -1
  22. package/payload/app/(dashboard)/members/README.md +2 -2
  23. package/payload/app/(dashboard)/members/page.tsx +4 -3
  24. package/payload/app/(dashboard)/operations-tickets/README.md +2 -2
  25. package/payload/app/(dashboard)/operations-tickets/page.tsx +2 -1
  26. package/payload/app/(dashboard)/releases/README.md +2 -2
  27. package/payload/app/(dashboard)/releases/page.tsx +2 -1
  28. package/payload/app/(dashboard)/requests/README.md +2 -2
  29. package/payload/app/(dashboard)/requests/[id]/README.md +2 -2
  30. package/payload/app/(dashboard)/requests/[id]/actions.ts +1 -1
  31. package/payload/app/(dashboard)/requests/[id]/page.tsx +8 -6
  32. package/payload/app/(dashboard)/requests/page.tsx +2 -1
  33. package/payload/app/(dashboard)/services/README.md +6 -6
  34. package/payload/app/(dashboard)/services/loading.tsx +1 -1
  35. package/payload/app/(dashboard)/services/page.tsx +17 -8
  36. package/payload/app/(dashboard)/settings/page.tsx +2 -1
  37. package/payload/app/(dashboard)/tests/README.md +2 -2
  38. package/payload/app/(dashboard)/tests/page.tsx +2 -1
  39. package/payload/app/(dashboard)/usage/README.md +5 -5
  40. package/payload/app/(dashboard)/usage/page.tsx +5 -4
  41. package/payload/app/api/assistant/route.ts +1 -1
  42. package/payload/app/api/export/requests/route.ts +1 -1
  43. package/payload/app/embed/home/page.tsx +2 -2
  44. package/payload/app/embed/home/view.tsx +1 -1
  45. package/payload/app/embed/now/README.md +1 -1
  46. package/payload/app/embed/now/page.tsx +3 -3
  47. package/payload/app/embed/now/view.tsx +1 -1
  48. package/payload/app/embed/requests/page.tsx +2 -2
  49. package/payload/app/embed/requests/view.tsx +1 -1
  50. package/payload/app/not-found.tsx +2 -1
  51. package/payload/app/sign-in/actions.ts +1 -1
  52. package/payload/app/sign-in/gated-panel.tsx +1 -1
  53. package/payload/app/sign-in/page.tsx +2 -1
  54. package/payload/app/system/(atlas)/[section]/[id]/page.tsx +1 -1
  55. package/payload/app/system/(atlas)/page.tsx +7 -1
  56. package/payload/docs/agents.md +3 -3
  57. package/payload/docs/assistant.md +2 -2
  58. package/payload/docs/atlas-scenario-coverage.md +32 -32
  59. package/payload/docs/benchmark.md +1 -1
  60. package/payload/docs/data-display.md +1 -0
  61. package/payload/docs/data-layers.md +285 -0
  62. package/payload/docs/start-a-dashboard.md +3 -1
  63. package/payload/docs/testing.md +2 -2
  64. package/payload/next.config.ts +12 -0
  65. package/payload/package.json +1 -1
  66. package/payload/scripts/assistant.ts +2 -2
  67. package/payload/scripts/check.local.ts +13 -0
  68. package/payload/scripts/check.ts +156 -12
  69. package/payload/scripts/lib/cls.ts +19 -0
  70. package/payload/scripts/lib/layer-rules.ts +121 -0
  71. package/payload/scripts/lib/routes.ts +2 -2
  72. package/payload/scripts/lib/sample.ts +3 -3
  73. package/payload/scripts/vitals.ts +5 -4
  74. package/payload/skills/zz-meridian/SKILL.md +6 -1
  75. package/payload/skills/zz-meridian/references/agents.md +3 -3
  76. package/payload/skills/zz-meridian/references/cache.md +87 -29
  77. package/payload/skills/zz-meridian/references/customize.md +19 -15
  78. package/payload/skills/zz-meridian/references/data-layers.md +286 -0
  79. package/payload/skills/zz-meridian/references/existing-project.md +10 -2
  80. package/payload/skills/zz-meridian/references/ownership.md +3 -2
  81. package/payload/skills/zz-meridian/references/update.md +4 -3
  82. package/payload/src/components/base/shell/index.tsx +21 -4
  83. package/payload/src/components/charts/bar-list/index.tsx +9 -1
  84. package/payload/src/components/charts/column-chart/index.tsx +18 -12
  85. package/payload/src/components/charts/composition-bar/index.tsx +2 -1
  86. package/payload/src/components/charts/sparkline/index.tsx +4 -1
  87. package/payload/src/components/charts/trend-chart/index.tsx +15 -9
  88. package/payload/src/components/patterns/activity-feed/index.tsx +3 -1
  89. package/payload/src/components/patterns/filter-bar/index.tsx +4 -1
  90. package/payload/src/components/patterns/incident-card/README.md +1 -1
  91. package/payload/src/components/patterns/incident-card/index.tsx +6 -12
  92. package/payload/src/components/patterns/metric-tile/index.tsx +2 -1
  93. package/payload/src/components/patterns/shell-tools/README.md +1 -1
  94. package/payload/src/components/patterns/status-list/README.md +1 -1
  95. package/payload/src/components/patterns/status-list/index.tsx +1 -3
  96. package/payload/src/components/ui/card/index.tsx +8 -3
  97. package/payload/src/components/ui/delta/index.tsx +5 -0
  98. package/payload/src/components/ui/key-value/index.tsx +8 -1
  99. package/payload/src/components/ui/status-dot/index.tsx +11 -3
  100. package/payload/src/data/README.md +2 -2
  101. package/payload/src/data/access.ts +39 -19
  102. package/payload/src/data/derived.ts +71 -0
  103. package/payload/src/data/engineering.ts +11 -5
  104. package/payload/src/data/projections.ts +140 -0
  105. package/payload/src/data/read.ts +48 -10
  106. package/payload/src/data/tickets.ts +10 -15
  107. package/payload/src/data/uptime.ts +49 -0
  108. package/payload/src/data/usage.ts +9 -4
  109. package/payload/src/lib/assistant/tools.ts +1 -1
  110. package/payload/src/lib/collection.ts +12 -4
  111. package/payload/src/lib/format.ts +18 -5
  112. package/payload/src/lib/safe-markdown.ts +20 -3
  113. package/payload/src/logic/after-start.ts +11 -0
  114. package/payload/src/{data → logic}/alerts.ts +8 -5
  115. package/payload/src/logic/derivations.ts +72 -0
  116. package/payload/src/{views → logic}/home-context.ts +1 -1
  117. package/payload/src/{views → logic}/home-parts.ts +1 -1
  118. package/payload/src/{views → logic}/keys-context.ts +3 -3
  119. package/payload/src/{views → logic}/now-context.ts +2 -2
  120. package/payload/src/{views → logic}/now-parts.ts +4 -3
  121. package/payload/src/{views → logic}/revenue-context.ts +1 -1
  122. package/payload/src/{views → logic}/revenue-parts.ts +1 -1
  123. package/payload/src/{views → logic}/services-context.ts +3 -2
  124. package/payload/src/{views → logic}/usage-context.ts +1 -1
  125. package/payload/src/{views → logic}/usage-parts.ts +1 -1
  126. package/payload/src/system/content.ts +1 -0
  127. package/payload/src/views/customer.tsx +1 -1
  128. package/payload/src/views/customers.tsx +1 -1
  129. package/payload/src/views/home.tsx +3 -1
  130. package/payload/src/views/keys.tsx +9 -9
  131. package/payload/src/views/members.tsx +1 -1
  132. package/payload/src/views/no-access.tsx +16 -8
  133. package/payload/src/views/now.tsx +2 -1
  134. package/payload/src/views/refusal.tsx +23 -0
  135. package/payload/src/views/releases.tsx +1 -1
  136. package/payload/src/views/request.tsx +1 -1
  137. package/payload/src/views/requests.tsx +1 -1
  138. package/payload/src/views/revenue.tsx +1 -1
  139. package/payload/src/views/sample-footer.tsx +12 -11
  140. package/payload/src/views/services.tsx +19 -9
  141. package/payload/src/views/shell-alerts.tsx +1 -1
  142. package/payload/src/views/tests.tsx +1 -1
  143. package/payload/src/views/tickets.tsx +1 -1
  144. package/payload/src/views/usage.tsx +2 -2
  145. package/payload/tests/assistant-access.test.ts +1 -1
  146. package/payload/tests/bypasses.test.ts +3 -2
  147. package/payload/tests/card-header-title.test.tsx +15 -0
  148. package/payload/tests/client-imports.test.ts +58 -11
  149. package/payload/tests/collection.test.ts +38 -1
  150. package/payload/tests/counts-and-lists.test.tsx +7 -7
  151. package/payload/tests/customer-figures.test.ts +1 -1
  152. package/payload/tests/derivations.test.ts +45 -0
  153. package/payload/tests/derived-answer.test.ts +69 -0
  154. package/payload/tests/embed-page-failure.test.tsx +1 -1
  155. package/payload/tests/embed-requests-provenance.test.tsx +1 -1
  156. package/payload/tests/empty-views.test.tsx +1 -1
  157. package/payload/tests/engineering-workflows.test.ts +1 -1
  158. package/payload/tests/env-not-shared.test.ts +24 -0
  159. package/payload/tests/explore-contract.test.ts +1 -1
  160. package/payload/tests/feature-definitions.test.ts +1 -1
  161. package/payload/tests/filter-bar-search.test.tsx +29 -0
  162. package/payload/tests/format.test.ts +20 -2
  163. package/payload/tests/key-value.test.tsx +13 -0
  164. package/payload/tests/logic-alerts.test.ts +50 -0
  165. package/payload/tests/logic-layer.test.ts +58 -0
  166. package/payload/tests/marks-name-what-they-encode.test.tsx +98 -0
  167. package/payload/tests/moved-paths-documented.test.ts +51 -0
  168. package/payload/tests/no-access.test.tsx +17 -4
  169. package/payload/tests/page-frame-title.test.tsx +31 -0
  170. package/payload/tests/plural.test.ts +1 -1
  171. package/payload/tests/prose.test.tsx +7 -0
  172. package/payload/tests/records-consumers.test.tsx +1 -1
  173. package/payload/tests/refusal-descriptor.test.ts +44 -0
  174. package/payload/tests/release-phone-cell.test.ts +1 -1
  175. package/payload/tests/release-wording.test.ts +1 -1
  176. package/payload/tests/roles-context.test.ts +1 -1
  177. package/payload/tests/sample-company.test.ts +1 -1
  178. package/payload/tests/setup.ts +12 -1
  179. package/payload/tests/shared-context.test.tsx +28 -28
  180. package/payload/tests/shell-alerts.test.ts +1 -1
  181. package/payload/tests/ticket-mutations.test.ts +1 -1
  182. package/payload/tests/ticket-owner.test.ts +2 -2
  183. package/payload/tests/titles-engineering.test.ts +2 -2
  184. package/payload/tests/titles.test.ts +4 -4
  185. package/payload/tests/usage-summary.test.tsx +3 -3
  186. package/payload/tests/verify-explore-surfaces.test.ts +2 -2
  187. package/payload/tests/verify-sample-surfaces.test.ts +2 -2
  188. package/payload/tests/view-tool-modules.test.ts +6 -6
  189. package/payload/tests/view-tool-needs.test.ts +1 -1
  190. package/payload/tests/workspace-access.test.ts +1 -1
  191. package/payload/tests/workspace-data-paths.test.ts +3 -3
  192. package/payload/tests/workspace-figures.test.ts +1 -1
  193. package/payload/tests/workspace-home.test.tsx +5 -5
  194. /package/payload/src/{views → logic}/customer-context.ts +0 -0
  195. /package/payload/src/{views → logic}/customer-explore.ts +0 -0
  196. /package/payload/src/{views → logic}/customers-context.ts +0 -0
  197. /package/payload/src/{views → logic}/customers-explore.ts +0 -0
  198. /package/payload/src/{views → logic}/fact-insight.ts +0 -0
  199. /package/payload/src/{views → logic}/home-explore.ts +0 -0
  200. /package/payload/src/{views → logic}/key-scopes.ts +0 -0
  201. /package/payload/src/{views → logic}/keys-explore.ts +0 -0
  202. /package/payload/src/{views → logic}/main-summary.ts +0 -0
  203. /package/payload/src/{views → logic}/members-context.ts +0 -0
  204. /package/payload/src/{views → logic}/members-explore.ts +0 -0
  205. /package/payload/src/{views → logic}/now-explore.ts +0 -0
  206. /package/payload/src/{views → logic}/releases-context.ts +0 -0
  207. /package/payload/src/{views → logic}/releases-explore.ts +0 -0
  208. /package/payload/src/{views → logic}/request-context.ts +0 -0
  209. /package/payload/src/{views → logic}/request-explore.ts +0 -0
  210. /package/payload/src/{views → logic}/requests-context.ts +0 -0
  211. /package/payload/src/{views → logic}/requests-explore.ts +0 -0
  212. /package/payload/src/{views → logic}/revenue-explore.ts +0 -0
  213. /package/payload/src/{views → logic}/services-explore.ts +0 -0
  214. /package/payload/src/{components/patterns/status-list → logic}/summarise.ts +0 -0
  215. /package/payload/src/{views → logic}/tests-context.ts +0 -0
  216. /package/payload/src/{views → logic}/tests-explore.ts +0 -0
  217. /package/payload/src/{views → logic}/tickets-context.ts +0 -0
  218. /package/payload/src/{views → logic}/tickets-explore.ts +0 -0
  219. /package/payload/src/{views → logic}/tools.ts +0 -0
  220. /package/payload/src/{views → logic}/usage-explore.ts +0 -0
package/README.md CHANGED
@@ -80,6 +80,11 @@ 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
+ The standard a project on Meridian is built to is
84
+ [data-layers.md](https://github.com/zhixuan312/zz-meridian/blob/main/docs/data-layers.md): the four layers between a
85
+ database and the screen, the contract a backend answers, and the six rules a project switches on in its own
86
+ `scripts/check.local.ts`.
87
+
83
88
  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
89
 
85
90
  ## What this package does not do
@@ -230,6 +230,60 @@ function viewToolCalls(src) {
230
230
  }
231
231
  // ── The registry ─────────────────────────────────────────────────────────────────────────────────────
232
232
  const GATE_AND_BUILD = ['gate', 'build'];
233
+ /**
234
+ * The thirty-eight modules 0.28.0 moved into `src/logic/`, as the path each left and the path it went to: the figures
235
+ * and sentences that were `src/views/`'s, the status list's summary, and the alerts the dashboard layout reads.
236
+ */
237
+ const MOVED_TO_LOGIC = [
238
+ ['src/data/alerts.ts', 'src/logic/alerts.ts'],
239
+ ['src/components/patterns/status-list/summarise.ts', 'src/logic/summarise.ts'],
240
+ ...['customer-context', 'customer-explore', 'customers-context', 'customers-explore', 'fact-insight',
241
+ 'home-context', 'home-explore', 'home-parts', 'key-scopes', 'keys-context', 'keys-explore', 'main-summary',
242
+ 'members-context', 'members-explore', 'now-context', 'now-explore', 'now-parts', 'releases-context',
243
+ 'releases-explore', 'request-context', 'request-explore', 'requests-context', 'requests-explore',
244
+ 'revenue-context', 'revenue-explore', 'revenue-parts', 'services-context', 'services-explore',
245
+ 'tests-context', 'tests-explore', 'tickets-context', 'tickets-explore', 'tools', 'usage-context',
246
+ 'usage-explore', 'usage-parts',
247
+ ].map((name) => [`src/views/${name}.ts`, `src/logic/${name}.ts`]),
248
+ ];
249
+ /** The paths those modules left, without their extension, as an import resolves them. */
250
+ const MOVED_FROM = new Set(MOVED_TO_LOGIC.map(([from]) => from.replace(/\.ts$/, '')));
251
+ /**
252
+ * The symbols that left a shipped card for the logic layer: `afterStart` was exported by the incident card's own module
253
+ * and `summarise` re-exported by the status list's. Both cards are Meridian's, so the update replaces them and a team
254
+ * file that imported either symbol from the card stops compiling — a break the moved paths alone do not report.
255
+ */
256
+ const REMOVED_CARD_EXPORTS = [
257
+ ['src/components/patterns/incident-card', 'afterStart'],
258
+ ['src/components/patterns/status-list', 'summarise'],
259
+ ];
260
+ /** Each moved module as "the path it left → the path it went to", for the migration's instruction. */
261
+ const MOVED_LIST = MOVED_TO_LOGIC.map(([from, to]) => `\`${from}\` → \`${to}\``).join(', ');
262
+ /**
263
+ * Whether one of the team's files reads a symbol that left a managed module — by naming it in an import list, through
264
+ * a namespace (`card.afterStart(…)`), or by handing it on with a re-export. `removedExportReaders` below reads the same
265
+ * three shapes for 0.16.0's removals; a symbol is named in all three, and each stops compiling once the module is replaced.
266
+ */
267
+ function readsRemovedExport(rel, src, module, symbols) {
268
+ const escapes = (name) => name.replace(/\$/g, '\\$');
269
+ const reading = importsOf(rel, src).some(({ from, names, namespace }) => from === module && (names.some((n) => symbols.includes(n))
270
+ || (namespace !== null && symbols.some((s) => new RegExp(`(?<![\\w$.])${escapes(namespace)}\\.${s}\\b`).test(src)))));
271
+ if (reading)
272
+ return true;
273
+ for (const m of src.matchAll(/^\s*export\s+(?:type\s+)?(?:\*|\{([^}]*)\})\s*from\s+(['"])([^'"]+)\2/gm)) {
274
+ if (resolvedImport(rel, m[3]) !== module)
275
+ continue;
276
+ // `export *` hands on every symbol, the removed one among them.
277
+ if (m[1] === undefined)
278
+ return true;
279
+ if (m[1].split(',').map((s) => s.trim().replace(/^type\s+/, '').split(/\s+as\s+/)[0]).some((n) => symbols.includes(n)))
280
+ return true;
281
+ }
282
+ return false;
283
+ }
284
+ /** The team's files that import a moved module from the path it left, or a symbol from a card it no longer lives on. */
285
+ const importsAMovedModule = (root) => matching(root, (rel, src) => importsOf(rel, src).some(({ from }) => MOVED_FROM.has(from))
286
+ || REMOVED_CARD_EXPORTS.some(([module, symbol]) => readsRemovedExport(rel, src, module, [symbol])));
233
287
  export const RELEASE_MIGRATIONS = [
234
288
  {
235
289
  id: 'verify-modes',
@@ -478,6 +532,14 @@ export const RELEASE_MIGRATIONS = [
478
532
  instructions: 'Bring `tests/table-cut-cell.test.tsx` over from the release as it has it. Since 0.26.0 `src/components/ui/table/index.tsx` lays a table out as a grid whose rows are subgrids: every column is as wide as its content, the spare is shared equally between the gaps, and a `grow` column\'s floor is `min-w-40` at every width, without the `@min-[672px]:min-w-64` it had from a 672px table. A test of your own that expects that class expects `min-w-40` alone. A `TableCell` of your own with a `colSpan` takes one track of the grid, so it draws in the first column only: a row that should run the width of the table, such as an empty state, is drawn outside the table, or as a `DataTable` `empty`.',
479
533
  checks: ['gate'],
480
534
  },
535
+ {
536
+ id: 'four-layers',
537
+ since: '0.28.0',
538
+ applies: importsAMovedModule,
539
+ summary: "The template's figures and sentences moved into `src/logic/`: thirty-eight modules left `src/views/` and `src/data/`.",
540
+ instructions: `Read "The four layers" and "The path an existing product follows" in references/data-layers.md. Thirty-eight modules moved into the logic layer, and each import of one in your own \`src/views/\` producers and pages names the path it left: ${MOVED_LIST}. Rewrite each import to the path the module has now, or move your own module beside the work it does. Two symbols left a card of Meridian's, and both must be rewritten because the update replaces the card: \`afterStart\`, which the incident card exported, is \`@/logic/after-start\`, and \`summarise\`, which the status list re-exported, is \`@/logic/summarise\`. Every other path your own files name is still there, so a project that imported neither symbol keeps building as it was. The six layer rules stay off until your own \`scripts/check.local.ts\` exports \`fourLayers = true\`; turn them on once your figures have moved, and the gate and the build are what show the work is done.`,
541
+ checks: GATE_AND_BUILD,
542
+ },
481
543
  ];
482
544
  /**
483
545
  * A test of the rail that stands over a fake `ResizeObserver` answering `observe` with nothing (0.16.2). The lookbehind
package/dist/ownership.js CHANGED
@@ -2,23 +2,40 @@ const HASH = /^sha256-[0-9a-f]{64}$/;
2
2
  const SEMVER = /^\d+\.\d+\.\d+$/;
3
3
  /** The library modules Meridian's components and gates import; the rest of src/lib belongs to the template's pages. */
4
4
  export const LIB = ['cn', 'format', 'format-date', 'period', 'color', 'host', 'preferences', 'csv', 'safe-markdown', 'logo', 'collection', 'explore', 'live', 'shared-context', 'agent-guidance', 'insight', 'facts'].map((n) => `src/lib/${n}.ts`);
5
- /** The single files adopt copies besides the folders and the library: all of them must be in a payload that is whole. */
6
- export const FIXED = [...LIB, 'src/lib/assistant/prompt.ts', 'src/views/console-chrome.tsx', 'tests/setup.ts'];
5
+ /**
6
+ * The single files adopt copies besides the folders and the library: all of them must be in a payload that is whole.
7
+ * The three `src/logic/` helpers are here because shipped components import them; the rest of `src/logic/` is the
8
+ * team's own figures and no other module under it is delivered.
9
+ */
10
+ export const FIXED = [
11
+ ...LIB, 'src/lib/assistant/prompt.ts', 'src/views/console-chrome.tsx', 'tests/setup.ts',
12
+ 'src/logic/after-start.ts', 'src/logic/derivations.ts', 'src/logic/summarise.ts',
13
+ ];
7
14
  /** The one rule, a plain-English line per clause. `references/ownership.md` renders it, so the guidance cannot drift. */
8
15
  export const OWNERSHIP = [
9
16
  'Managed: the payload files under `tokens/`, `src/styles/` and `src/components/`, except each component\'s `README.md` and `preview.tsx`.',
10
17
  'Managed: the distributed scripts under `scripts/`, except `scripts/verify.config.ts`, the size baseline `optional:scripts/verify.baseline.json` (first-load JS and the HTML of each page) and a team-local `optional:scripts/check.local.ts`.',
11
18
  'Managed: the library helpers `src/lib/cn.ts`, `src/lib/format.ts`, `src/lib/format-date.ts`, `src/lib/period.ts`, `src/lib/color.ts`, `src/lib/host.ts`, `src/lib/preferences.ts`, `src/lib/csv.ts`, `src/lib/safe-markdown.ts`, `src/lib/logo.ts`, `src/lib/collection.ts`, `src/lib/explore.ts`, `src/lib/live.ts`, `src/lib/shared-context.ts`, `src/lib/agent-guidance.ts`, `src/lib/insight.ts` and `src/lib/facts.ts`, and the assistant prompt `src/lib/assistant/prompt.ts`.',
12
- 'Managed: `src/views/console-chrome.tsx` and `tests/setup.ts`.',
19
+ 'Managed: the three `src/logic/` helpers a shipped component imports — `src/logic/after-start.ts`, `src/logic/derivations.ts` and `src/logic/summarise.ts` — so a payload\'s cards build in an adopted project; every other module under `src/logic/` is the team\'s.',
20
+ 'Managed: `src/views/console-chrome.tsx`, `tests/setup.ts`, and — in a project `create` made — every test the release ships. A shipped test pins the behaviour of the module it covers, so a release that changes one changes the other; a test the team edits is staged for its decision like any other edited file.',
13
21
  'Managed: `optional:scripts/package.json`, only when the adopt or create that set the project up generated it for this project shape.',
14
22
  'Managed: both installed skill trees, `optional:.agents/skills/zz-meridian/` and `optional:.claude/skills/zz-meridian/`, taken from the release\'s skill payload.',
15
23
  'Managed: the brand token and stylesheet paths under `tokens/` and `src/styles/` that the release generated from the recorded brand.',
16
- 'Team-owned: every other product path, for example `src/app.config.ts`, `optional:src/views/`, `optional:src/data/`, `optional:app/`, `optional:docs/`, `AGENTS.md` and `package.json`.',
24
+ 'Team-owned: every other product path, for example `src/app.config.ts`, `optional:src/views/`, `optional:src/data/`, `optional:src/logic/`, `optional:app/`, `optional:docs/`, `AGENTS.md` and `package.json`.',
17
25
  'Team-preserved: a path that was once recorded but is no longer managed, and any team file inside a managed folder, is never changed or deleted by an update.',
18
26
  'Kept: a path in the keep register (`optional:.meridian/keep.json`) is left as the team has it. When the new release removes it, it is retired-kept: still left, and recorded as retired.',
19
27
  ];
20
28
  /** The rule as a markdown bullet list, for `references/ownership.md`. */
21
29
  export const renderOwnership = () => OWNERSHIP.map((l) => `- ${l}`).join('\n');
30
+ /**
31
+ * The managed set as one path pattern, for a caller that holds a manifest but no payload list — the consumer smoke checks
32
+ * a manifest read from a tarball it never enumerates. `managedPaths` is the rule; this is that rule said once more in a
33
+ * regex, and `tests/cli-ownership.test.ts` holds the two together over the payload paths it knows. The tests clause is the
34
+ * `create` route's, so it is permissive for an adopted project — whose manifest never names a test but `tests/setup.ts`.
35
+ */
36
+ export const MANAGED_PATH = /^(tokens\/|src\/styles\/|src\/components\/|scripts\/|src\/lib\/|\.agents\/|\.claude\/)|^(src\/views\/console-chrome\.tsx|tests\/(?:[^/]+\.test\.tsx?|setup\.ts)|src\/logic\/(?:after-start|derivations|summarise)\.ts)$/;
37
+ /** A path inside the managed folders that is the team's all the same: their own views, their app, their docs, their config. */
38
+ export const NEVER_MANAGED = (p) => p === 'src/app.config.ts' || p === 'scripts/verify.config.ts' || p.startsWith('app/') || p.startsWith('docs/') || (p.startsWith('src/views/') && p !== 'src/views/console-chrome.tsx');
22
39
  /** The raw `.meridian/manifest.json`, checked field by field; anything off throws an Error that names it. */
23
40
  export function parseManifest(text) {
24
41
  let raw;
@@ -67,8 +84,16 @@ export function adoptSetOf(payloadFiles) {
67
84
  * routes. `generated` is every path a replay or a real run produced; only the ones under `tokens/` or `src/styles/`
68
85
  * and `scripts/package.json` become managed, so a generated page or config stays the team's.
69
86
  */
70
- export function managedPaths(payloadFiles, _route, generated = []) {
87
+ export function managedPaths(payloadFiles, route, generated = []) {
71
88
  const out = new Set(adoptSetOf(payloadFiles));
89
+ // A created project's tests are Meridian's: it wrote every one of them, and each pins the behaviour of the module it
90
+ // covers. Left the team's, a release that changed `splitFigure` could not change the test that reads it, and the
91
+ // project's own gate failed after an update that was otherwise correct (0.29.0's consumer smoke). Adopt still copies
92
+ // none of them: that project has a test suite of its own.
93
+ if (route === 'create')
94
+ for (const f of payloadFiles)
95
+ if (/^tests\/[^/]+\.test\.tsx?$/.test(f))
96
+ out.add(f);
72
97
  for (const g of generated)
73
98
  if (g.startsWith('tokens/') || g.startsWith('src/styles/') || g === 'scripts/package.json')
74
99
  out.add(g);
package/dist/session.js CHANGED
@@ -154,7 +154,15 @@ function plan(ctx, manifest, startedAt) {
154
154
  const inBase = baseManaged.has(p);
155
155
  const inTarget = targetManaged.has(p);
156
156
  const managed = inBase || inTarget;
157
- const recorded = manifest.files[p] ?? (inBase && !sourcePayload.has(p) ? source.hashes.get(p) : undefined) ?? null;
157
+ // What the recorded release wrote at this path: the manifest's hash, or — for a project `create` made, which received
158
+ // every payload file — the recorded release's own bytes. The second term is what made 0.29.0's tests unclassifiable:
159
+ // a test that release shipped but did not record (it managed none but `tests/setup.ts` then) had no hash to compare
160
+ // with, so an unedited copy of it collided instead of being replaced, and the update stopped for a decision on a file
161
+ // nobody had touched.
162
+ const recorded = manifest.files[p]
163
+ ?? (manifest.route === 'create' && sourcePayload.has(p) ? source.hashes.get(p) : undefined)
164
+ ?? (inBase && !sourcePayload.has(p) ? source.hashes.get(p) : undefined)
165
+ ?? null;
158
166
  const targetHash = inTarget ? target.hashes.get(p) ?? null : null;
159
167
  if (targetHash)
160
168
  targetFiles[p] = targetHash;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zz-meridian",
3
- "version": "0.27.1",
3
+ "version": "0.29.0",
4
4
  "description": "Bring ZZ Meridian, a dashboard design system, into a Next.js project, or start a new dashboard on it. Copies the files in; nothing depends on this package at runtime.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -2,6 +2,42 @@
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.29.0] · 2026-10-25
6
+
7
+ ### New
8
+
9
+ - **A mark says what it encodes** ([#94](https://github.com/zhixuan312/zz-meridian/issues/94)). A colour, a fill or a size a reader is meant to decode now carries its words, built from the data the component already holds, so it is right at the call site rather than added page by page. A Bar list row names its row, its figure and its share (`Checkout · 420 · 42.0% of 1,000`, and without a total too); a Trend chart's marked day names the day and what it is marked for; its dashed stretch into a period still running says "Today so far, dashed"; a Sparkline says the range it draws, which was the one chart carrying no readout; a Delta says whether its colour reads better or worse, which the colour alone could not; a Status dot takes a `label` and becomes an image to assistive technology where it stands with no word beside it, and an activity's tone says what it means ("Went well", "Needs attention", "Failed"). `ColumnChart` and `BarList` take `highlightLabel` — what makes the accented row or column the one the card is about — and say it on that mark and in the hidden table's caption. `docs/data-display.md`'s `## Marks` now states the rule, so a new chart is held to it.
10
+
11
+ ### Fixed
12
+
13
+ - **A card's head never draws its title over its controls** ([#87](https://github.com/zhixuan312/zz-meridian/issues/87)). `CardHeader` gave the title `min-w-0 flex-1` and the actions `shrink-0`, so in a narrow column the title's box collapsed: at 1024px the second column of `<Row split="2/3">` is 229px, a head with a `Select size="sm"` and two labels took 138 of them, and the title's first word was drawn 32px past its own box, across the controls. The head wraps now, and the title's box has a floor (8rem, the width a word or two needs), so the actions drop to their own line when the two cannot share one. A title that wraps breaks a word too long for the line instead of drawing it over its neighbour.
14
+ - **A page headed by a sentence that opens with its own name announces it once** ([#88](https://github.com/zhixuan312/zz-meridian/issues/88)). `PageTitle` drew the name out of sight and then the sentence, so "USDC: no rule met…" was read as "USDC : USDC: no rule met…" in the heading and twice in a screen reader's list of headings. A sentence that is a plain string is compared with the name, and one that reads data takes `speaksName` on `PageTitle` or `PageFrame`; the name stays in the markup either way, so the heading is in the prerendered shell, and a sentence still to come leaves the name read.
15
+ - **A Key value list wraps a value on a phone instead of cutting it behind a title no phone shows** ([#89](https://github.com/zhixuan312/zz-meridian/issues/89)). Every value truncated unless its item set `wrap`, with the whole of it in the row's `title` — which a pointer reads and a phone cannot. Below the width where a sentence value cannot fit beside its 8rem label (26rem of row) the value wraps as `wrap` asks for; above it the one line and the title stay.
16
+ - **The CLS observer bounds a session window at 5 s** ([#90](https://github.com/zhixuan312/zz-meridian/issues/90)). It summed shifts into a session while each was within 1 s of the last, which is half of web-vitals' definition: a page that filled in over half a minute reported one session the length of the page, so `verify --timed` could fail a page inside its budget and the number could not be compared with another tool's. `scripts/lib/cls.ts` is the rule now, both bounds with it, and `tests/cli-vitals-cls.test.ts` runs the source the page runs.
17
+ - **A collection may hide or page-own a field it computes** ([#91](https://github.com/zhixuan312/zz-meridian/issues/91)). `visibleFields` and `writableFields` built their `omit` masks from the names alone, so a `hidden` or `pageOnlyFields` entry for a joined label or a derived total — which both doc comments allow — threw on a key the shape never had, on every render. A name the shape does not hold is skipped; the ones it holds are still masked, and the write schema stays strict, so a hidden or page-owned field sent anyway is refused.
18
+ - **An image's address is read the way the URL parser reads it** ([#92](https://github.com/zhixuan312/zz-meridian/issues/92)). `safe-markdown` accepted a `src` with no scheme that was not written `//`, so `\\host/p.png`, `/\host/p.png` and `\/host/p.png` passed while the browser resolved each to `//host/p.png` and fetched a third party — the case the rule exists to stop. The address is now resolved against a fixed origin with the backslashes normalised, and kept only when it lands back on it.
19
+ - **A filter bar's search has a floor, so its hint reads the same on a 2x display as on a 1x one** ([#93](https://github.com/zhixuan312/zz-meridian/issues/93)). The field took whatever five facets left: 129px at a 1440px window on a Retina screen, where the placeholder needs 134, so the same page read "Headline, address, ru" there and the whole hint on a laptop. It is 10rem wide at least, and the facets truncate their own labels to make the room.
20
+ - **The environment is put back before every test** ([#95](https://github.com/zhixuan312/zz-meridian/issues/95)). Files share a worker (`pool: 'vmThreads'`), so a file that stubbed a store's variables in a `beforeAll` left them set for the file beside it — seen as an intermittent gate where a view test found a store configured and asked a stub for a snapshot. `tests/setup.ts` restores the environment before each test, so a stub belongs in a `beforeEach`, which runs after it.
21
+ - **A created project's tests move with the release that changes them.** A test the template ships pins the behaviour of the module it covers, and every shipped test but `tests/setup.ts` was the team's, so an update could change `src/lib/format.ts` and leave `tests/format.test.ts` asserting what it did before: a project created with 0.28.0 and updated to 0.29.0 failed its own gate on `splitFigure`. `create` now records the tests it writes as managed, so an update replaces an unedited one with the release's; a test your team edited is staged for your decision, as any other edited file is. `adopt` still copies none of them, because a project it joins has a suite of its own.
22
+
23
+ - **A figure keeps its symbol and its sign when it is split or shortened, and an amount too small for 20 places never reads as zero** ([#96](https://github.com/zhixuan312/zz-meridian/issues/96)). `splitFigure` failed on a leading mark-and-sign figure (`-$298.43` came back whole), so a tile set it in its number face, or lost both the `$` and the minus when it shortened; `fitFigure` re-built a shortened figure with the app's own currency, so `HK$12,345,678` read `$12.3M`. The minus belongs to the mark, and a figure shortens with the mark and the sign it was given. `formatCost` below 5e-5 keeps its two leading digits in exponent form (`$1.0e-21`) where `toFixed` printed a row of zeroes.
24
+
25
+ ## [0.28.0] · 2026-10-24
26
+
27
+ ### Breaking
28
+
29
+ - **The four data layers are the standard, and the template's figures moved into `src/logic/`.** Thirty-eight modules that computed a figure or wrote a sentence — thirty-six under `src/views/`, the status list's `summarise`, and the alerts the dashboard layout reads — live in `src/logic/` now, and every import names the path each has. `docs/data-layers.md` is the standard: the four layers between a database and the screen, the contract a backend answers, the seven direct reads, and the order a product with its own API or tables follows.
30
+ - **What a team does.** Run `update`; it reports `migration:four-layers` for each of your own files that still imports a module from a path the move left, and its instruction names all thirty-eight with the path each left. Rewrite each of those imports in your own `src/views/` producers and pages to `src/logic/`, or move the module itself beside the work it does. Two symbols left the cards they were exported from and must move with them: `afterStart` is `@/logic/after-start`, and `summarise` is `@/logic/summarise`.
31
+ - **Doing nothing.** A project that imported neither symbol keeps building: the move is in this template, not in your project, so every path your own files name is still there. A file of yours that imported `afterStart` from `src/components/patterns/incident-card`, or `summarise` from `src/components/patterns/status-list`, stops compiling — both cards are Meridian's and the update replaces them. What no project gets by doing nothing is the layer: your figures stay where they were, and the six rules below stay silent about them until you move them and say so.
32
+ - **Three helpers arrive managed.** `src/logic/after-start.ts`, `src/logic/derivations.ts` and `src/logic/summarise.ts` are Meridian's from this release, because a shipped card imports them: `update` replaces each when you have not edited it, and stages base, ours and new when you have. Every other module under `src/logic/` is yours.
33
+ - **Six rules, off until you ask.** `node scripts/check.ts` checks `data-imports-neither`, `logic-does-not-import-views`, `views-produce-nothing`, `presentation-does-not-read`, `no-direct-query` and `client-does-not-import-a-reading-module`. Every one is silent until your own `scripts/check.local.ts` exports `fourLayers = true`; until then the check runs exactly as it did before. `docs/data-layers.md`'s "The six rules, and the switch" says what each asks and what skipping it costs.
34
+
35
+ ### Fixed
36
+
37
+ - **The first load carried a second copy of the shell.** Turbopack's chunker merges small chunks and duplicates what it cannot merge: after the data layer stopped importing `@/views`, every route's first load held a second copy of the shell and the shared UI set — 40.3 and 29.9 KiB of it on `/engineering` — and three routes went over budget, `/engineering` 832 KiB against the 820 KiB cap, `/business` at the cap, and `/customers/[id]` 5.6% over its baseline. Smaller chunks are reusable across pages, so `experimental.turbopackChunking` now asks the chunker for them (`minChunkSize: 35000`, `maxChunkCountPerGroup: 80`): `/engineering` 811 KiB, and a page asks for 27 chunks where it asked for 23. A chunk is also a script tag in the page's HTML, so the split is taken as coarse as the HTML budget allows — every page stays within 3.8% of its baseline HTML, `/keys` the closest — and no cap, baseline or page was moved.
38
+
39
+ - **A created project's `pnpm verify` no longer fails on this repository's own record.** Two checks that ship with the template read files only this checkout has — the changelog beside the CLI's migrations and their tests, and the five guides the skill is delivered from, which `create` installs at `.claude/skills/` and `.agents/skills/` instead. Each now asks only where its subject exists, which is a checkout carrying `cli/src/migrations.ts`: a created or adopted project has no such record to check, and in this repository both run exactly as they did.
40
+
5
41
  ## [0.27.1] · 2026-10-24
6
42
 
7
43
  ### Fixed
package/payload/README.md CHANGED
@@ -35,6 +35,8 @@ These govern every layer. When two choices both fit the specs, pick the one that
35
35
 
36
36
  A change at the bottom moves everything above it, and nothing above may invent a value of its own.
37
37
 
38
+ The **four data layers** answer a different question: where an answer comes from. The database, `src/data/` (the one place a product's backend is touched), `src/logic/` (the business logic) and what draws it. They describe the path between records and the screen, so a data layer is not a content layer and adds nothing to the count above. `docs/data-layers.md` is the standard, and the skill ships the same text.
39
+
38
40
  ## Three surfaces, two operators
39
41
 
40
42
  | Surface | Where | Navigation |
@@ -24,11 +24,11 @@ No other chart card is on Home: the busiest endpoints, the responses by status c
24
24
 
25
25
  ### The year's card
26
26
 
27
- Its title states the year: "Requests grew 105.7% in a year, and p95 stayed between 277 and 294 ms" in the sample at 30 days, from the first window to the latest and over the windows' p95 in whole milliseconds; with fewer than two windows, as with All, "Requests and latency, a period at a time". The sentence starts at the first window that begins on or after the first day with a request (`yearLine` in `src/views/home-parts.ts`); a quiet day after that keeps its window. A window that holds the first request after days with none is not whole: its requests are short and its median day may have no p95, so measured from it flat traffic would read as growth and p95 as 0. When the days kept begin before the first request, the sentence grows from the first whole window and names it ("Requests grew … from the 30 days to 14 Sept 2026, and p95 stayed between …", a form the sample, whose first day has requests, never takes), its p95 range is over the windows from there, and with fewer than two windows from that one to the latest it is the sentence for fewer than two windows. A change that rounds to 0.0% reads "Requests held level". Now states the same sentence, at 30 days, among the facts of the band. A window is a whole period of the period's length counted back from yesterday over every complete day kept: 51 of 7 days, 12 of 30, 4 of 90, one of All. Its line says how long a column is. Revenue a period takes the same windows and the same cursor: point at a window in either chart and the three read it.
27
+ Its title states the year: "Requests grew 105.7% in a year, and p95 stayed between 277 and 294 ms" in the sample at 30 days, from the first window to the latest and over the windows' p95 in whole milliseconds; with fewer than two windows, as with All, "Requests and latency, a period at a time". The sentence starts at the first window that begins on or after the first day with a request (`yearLine` in `src/logic/home-parts.ts`); a quiet day after that keeps its window. A window that holds the first request after days with none is not whole: its requests are short and its median day may have no p95, so measured from it flat traffic would read as growth and p95 as 0. When the days kept begin before the first request, the sentence grows from the first whole window and names it ("Requests grew … from the 30 days to 14 Sept 2026, and p95 stayed between …", a form the sample, whose first day has requests, never takes), its p95 range is over the windows from there, and with fewer than two windows from that one to the latest it is the sentence for fewer than two windows. A change that rounds to 0.0% reads "Requests held level". Now states the same sentence, at 30 days, among the facts of the band. A window is a whole period of the period's length counted back from yesterday over every complete day kept: 51 of 7 days, 12 of 30, 4 of 90, one of All. Its line says how long a column is. Revenue a period takes the same windows and the same cursor: point at a window in either chart and the three read it.
28
28
 
29
29
  ### The title
30
30
 
31
- The page is headed by what its revenue says for the period chosen, worked out by `homeTitle` in `src/views/home-context.ts`. The heading reads "Home: " before it, out of sight, and the top bar shows "Home".
31
+ The page is headed by what its revenue says for the period chosen, worked out by `homeTitle` in `src/logic/home-context.ts`. The heading reads "Home: " before it, out of sight, and the top bar shows "Home".
32
32
 
33
33
  | When | The title |
34
34
  |---|---|
@@ -42,7 +42,7 @@ In the sample the four periods read "The highest 7 days yet: $810,137.13", "The
42
42
 
43
43
  ### The line under the title
44
44
 
45
- One line, worked out by `homeLine` in `src/views/home-context.ts`: the customers that have not left, the period's requests and their change, and gross margin for a person who may read what compute cost, with the services last. Counts are written at a glance (`formatCompact`). A service that is not operational, or a product monitoring none, is the line's own second sentence, after the figures: on a day whose title states a record it is the only place on the page that says what is wrong today. In the sample at 30 days an Owner reads "320 customers, 1.1B requests in 30 days (up 4.7%), and a gross margin of 62.4%. Inference API is degraded." and a Member or a Viewer the same line without the margin clause. A clause whose figures could not be read is left out.
45
+ One line, worked out by `homeLine` in `src/logic/home-context.ts`: the customers that have not left, the period's requests and their change, and gross margin for a person who may read what compute cost, with the services last. Counts are written at a glance (`formatCompact`). A service that is not operational, or a product monitoring none, is the line's own second sentence, after the figures: on a day whose title states a record it is the only place on the page that says what is wrong today. In the sample at 30 days an Owner reads "320 customers, 1.1B requests in 30 days (up 4.7%), and a gross margin of 62.4%. Inference API is degraded." and a Member or a Viewer the same line without the margin clause. A clause whose figures could not be read is left out.
46
46
 
47
47
  The revenue card and the tiles share the page's Meridian: its dates are the period's complete days, then the day in progress. Point at a day in the revenue chart and the featured figure reads it; at the day in progress its pill says "so far". A tile reads the pointed day only when its points are the Meridian's dates (the Metric tile's rule). While a day is in progress, the Requests, Compute cost and Gross margin tiles have a point for each complete day and none for the day in progress, one short of the Meridian's dates, so they keep the period's figure and a part-day is never read as a whole one. When the latest day kept is complete, no day is in progress, their points are the Meridian's dates, and pointing at a day reads it in those three tiles too.
48
48
 
@@ -63,12 +63,12 @@ Below 1024px every row stacks: the revenue card first, then its three tiles (thr
63
63
 
64
64
  ## Data
65
65
 
66
- Every figure is read on the server by the Home tool's read (`homeTool` in `src/views/tools.ts`), which needs every grant it reads (`FEATURES.home.needs`: the days, the usage by model, the models, compute, customers, services, incidents, tickets, verifications and releases; neither members nor keys nor what compute cost); a person lacking one is refused before anything is read. Each collection is read apart from the others through `read()` or `readAll()`, so one that cannot be read leaves its part saying so (`unread` on the page's data names it) and the rest standing. `homeData` in `src/views/home-parts.ts` turns the rows into what the parts draw: figures, lines and the points of their charts, never a row of the customers, the usage, the compute or its cost.
66
+ Every figure is read on the server by the Home tool's read (`homeTool` in `src/logic/tools.ts`), which needs every grant it reads (`FEATURES.home.needs`: the days, the usage by model, the models, compute, customers, services, incidents, tickets, verifications and releases; neither members nor keys nor what compute cost); a person lacking one is refused before anything is read. Each collection is read apart from the others through `read()` or `readAll()`, so one that cannot be read leaves its part saying so (`unread` on the page's data names it) and the rest standing. `homeData` in `src/logic/home-parts.ts` turns the rows into what the parts draw: figures, lines and the points of their charts, never a row of the customers, the usage, the compute or its cost.
67
67
 
68
- - **The period's figures** come from `readDays(period)`: the complete days counted back from yesterday, their totals and those of the same number of days before, and the day in progress on its own, counted in nothing. Revenue (a day's average and the best day), tokens served, requests and the days inside the band (`steady` with `facts.band`) are of those days. Compute in use is the GPU-hours used over those provisioned (`gpus × 24`) of the `compute` rows of the same days, with the fullest region; what we sell is the `usage` rows' revenue by model over the same days, the largest first, each with its share of the whole; past the four slots (`SOLD_SLOTS` in `src/views/home-context.ts`) the rest are summed into one part, "Other", which the page draws and the context states with the same shares.
68
+ - **The period's figures** come from `readDays(period)`: the complete days counted back from yesterday, their totals and those of the same number of days before, and the day in progress on its own, counted in nothing. Revenue (a day's average and the best day), tokens served, requests and the days inside the band (`steady` with `facts.band`) are of those days. Compute in use is the GPU-hours used over those provisioned (`gpus × 24`) of the `compute` rows of the same days, with the fullest region; what we sell is the `usage` rows' revenue by model over the same days, the largest first, each with its share of the whole; past the four slots (`SOLD_SLOTS` in `src/logic/home-context.ts`) the rest are summed into one part, "Other", which the page draws and the context states with the same shares.
69
69
  - **What compute cost** is read only when the person may read it: the read asks `can(scope, 'costs', 'read')` once, the question `read()` and the assistant's query ask, and reads `costs` only on a yes. Then Compute cost is the period's `compute` summed, and Gross margin is revenue less compute cost, over revenue, over the same complete days, each with its change. For anyone else the page's data has no key that names a cost or a margin, the line under the title has no margin clause, and the context says neither: nothing stands in their place.
70
70
  - **The year** comes from every complete day kept (`readDays('all')`): whole windows of the period's length counted back from yesterday (51 of 7 days, 12 of 30, 4 of 90, one of All), each with its requests, the median day's p95 and its revenue, the windows' median p95 for the latency chart's baseline, and the year card's title from the first window to the latest. The title of the page, its record and the facts of a year are worked out from the same days, on the server, and the page's client is handed the sentences and never the history.
71
- - **The customers** are every customer that has not left: how many, how many active, on a trial and past due, and the `all` fact (`allSentMore` in `src/views/customers-context.ts`, the one Revenue is headed by) as the tile's finding when it holds.
71
+ - **The customers** are every customer that has not left: how many, how many active, on a trial and past due, and the `all` fact (`allSentMore` in `src/logic/customers-context.ts`, the one Revenue is headed by) as the tile's finding when it holds.
72
72
  - **Business and Engineering** each hand four lines of a label and a figure (a figure that could not be read is a dash) and one counted fact: Revenue, Customers, On a trial and Past due with the `all` fact or how many customers sent more this week than last; Services operational, Incidents open, Releases in testing and Tickets open with the days inside the band over every complete day kept.
73
73
  - **Worth knowing across the company** is at most four lines, in this order, each with the address where it is handled: the newest incident nobody has resolved (`/engineering`); the largest customer paying `facts.concentrationAt` or more of the revenue of the last 30 complete days, whatever the period, with how many others do (`/customers/<id>`; no other line states a customer's share); a release that has passed every required check and has not gone out, as the Releases page stages it (`/releases?state=ready`); and the customers past due, with what they were billed. With none, one line says so.
74
74
 
@@ -84,7 +84,7 @@ The body registers the ten collections its feature needs (`days`, `usage`, `mode
84
84
 
85
85
  ## Agents
86
86
 
87
- The page shares one context with both agents (decision 0011), built by `homeContext` in `src/views/home-context.ts` from the figures the page draws. The page states each fact of a year in the line the context carries, under the same kind, so a person and an agent read one sentence. The activity feed, which marks an agent's own changes, is not on Home: it is one team's detail, and it moves to Now.
87
+ The page shares one context with both agents (decision 0011), built by `homeContext` in `src/logic/home-context.ts` from the figures the page draws. The page states each fact of a year in the line the context carries, under the same kind, so a person and an agent read one sentence. The activity feed, which marks an agent's own changes, is not on Home: it is one team's detail, and it moves to Now.
88
88
 
89
89
  ### What the agent reads
90
90
 
@@ -124,7 +124,7 @@ The page marks the revenue card `data-protagonist="Revenue"` on a wrapper that i
124
124
  - "How many customers are on each plan, and what did each plan pay?" — the customers that have not left and their revenue over the last 30 complete days, by plan
125
125
  - "How full were the GPUs in each region over the last 90 days?" — compute in use over 90 complete days, by region, against the 90 days before
126
126
 
127
- Home's prepared query (`homeExplore` in `src/views/home-explore.ts`) reads five sources. `days` is windowed by complete days and bucketed by day, week, month or weekday, with revenue, requests, tokens served and gross margin, a margin of the days' revenue and what compute cost over the same window. `usage` is the same days by model, `compute` by region, with the share of GPU-hours used, and `costs` is what compute cost a day. `customers` has no date: each customer's figures cover the last 30 complete days, narrowed and grouped by plan, status, region and owner. What compute cost, and so gross margin, is offered only to an Owner and an Admin: for anyone else the `costs` source and the gross margin are absent from the tool. A drilled day opens Home pointed at it, and a drilled customer its own page.
127
+ Home's prepared query (`homeExplore` in `src/logic/home-explore.ts`) reads five sources. `days` is windowed by complete days and bucketed by day, week, month or weekday, with revenue, requests, tokens served and gross margin, a margin of the days' revenue and what compute cost over the same window. `usage` is the same days by model, `compute` by region, with the share of GPU-hours used, and `costs` is what compute cost a day. `customers` has no date: each customer's figures cover the last 30 complete days, narrowed and grouped by plan, status, region and owner. What compute cost, and so gross margin, is offered only to an Owner and an Admin: for anyone else the `costs` source and the gross margin are absent from the tool. A drilled day opens Home pointed at it, and a drilled customer its own page.
128
128
 
129
129
  ## Embed view
130
130
 
@@ -6,9 +6,10 @@ import { PeriodSelect } from '@/components/patterns/period-select';
6
6
  import { HomeBody } from '@/views/home';
7
7
  import { PeriodActionsRoom, PeriodRoom } from '@/views/period-actions';
8
8
  import { gate, gatedMetadata } from '@/data/access';
9
+ import { RefusalScreen } from '@/views/refusal';
9
10
  import { DEMO_NOW, DEMO_UPDATED_AT } from '@/data/sample';
10
- import { dayRows } from '@/views/home-context';
11
- import { homeTool } from '@/views/tools';
11
+ import { dayRows } from '@/logic/home-context';
12
+ import { homeTool } from '@/logic/tools';
12
13
  import { cache } from 'react';
13
14
  import { parsePeriod, type Period } from '@/lib/period';
14
15
  import { app, workspaces } from '@/app.config';
@@ -21,7 +22,7 @@ export function generateMetadata() {
21
22
 
22
23
  type SearchParams = Promise<{ period?: string; day?: string }>;
23
24
 
24
- /** The page's one read, through the Home tool (`src/views/tools.ts`), shared by the title, the line, the export and the body. */
25
+ /** The page's one read, through the Home tool (`src/logic/tools.ts`), shared by the title, the line, the export and the body. */
25
26
  const readPeriod = cache((period: Period, day: string | undefined) => homeTool.read({ period, day: /^\d{4}-\d{2}-\d{2}$/.test(day ?? '') ? day : undefined }));
26
27
  /**
27
28
  * Every part reads the page through this, so each asks `readPeriod` with the same two arguments: `cache` keys first on
@@ -42,7 +43,7 @@ const readHome = async (searchParams: SearchParams) => {
42
43
  */
43
44
  export default async function HomePage({ searchParams }: { searchParams: SearchParams } = { searchParams: Promise.resolve({}) }) {
44
45
  const denied = await gate('home');
45
- if (denied) return denied;
46
+ if (denied) return RefusalScreen(denied);
46
47
  return (
47
48
  <PageFrame
48
49
  kicker={<>{app.name} · {app.workspace}</>}
@@ -31,7 +31,7 @@ The period control moves what is read from the daily records: the title's revenu
31
31
 
32
32
  ### The title
33
33
 
34
- The page is headed by what its customers and its revenue say for the period chosen, worked out by `revenueTitle` in `src/views/revenue-context.ts`. The heading reads "Revenue: " before it, out of sight, and the top bar shows "Revenue".
34
+ The page is headed by what its customers and its revenue say for the period chosen, worked out by `revenueTitle` in `src/logic/revenue-context.ts`. The heading reads "Revenue: " before it, out of sight, and the top bar shows "Revenue".
35
35
 
36
36
  | When | The title |
37
37
  |---|---|
@@ -62,10 +62,10 @@ Below 1024px (`lg`) every row stacks, the Featured metric first. The three tiles
62
62
 
63
63
  ## Data
64
64
 
65
- Every figure is read on the server by the Revenue tool's read (`revenueTool` in `src/views/tools.ts`, which takes `period`, 30d when not given). Each collection is read apart from the others, so one that cannot be read leaves its part saying so (`unread` on the page's data) and the rest standing. `revenueData` in `src/views/revenue-parts.ts` turns the rows into what the parts draw, never a customer's record: the wall gets the eight of each order, the ladder each stage's two named customers (three when it holds three) and what each other customer paid, which the Bar list folds into "N others".
65
+ Every figure is read on the server by the Revenue tool's read (`revenueTool` in `src/logic/tools.ts`, which takes `period`, 30d when not given). Each collection is read apart from the others, so one that cannot be read leaves its part saying so (`unread` on the page's data) and the rest standing. `revenueData` in `src/logic/revenue-parts.ts` turns the rows into what the parts draw, never a customer's record: the wall gets the eight of each order, the ladder each stage's two named customers (three when it holds three) and what each other customer paid, which the Bar list folds into "N others".
66
66
 
67
67
  - **The period's figures** come from `readDays(period)` and its `revenueByPlan`; the windows, the year ago and the best day from every complete day kept (`readDays('all')`), read on the server, and the page is handed the sentences and figures, never the history.
68
- - **The customers' figures** are every customer that has not left (`kept`), from the `customers` collection: what each paid in the last 30 complete days, its 14-day trend and its week (`week` in `src/views/customers-context.ts`). Those that have left are counted in nothing but the ladder's "left".
68
+ - **The customers' figures** are every customer that has not left (`kept`), from the `customers` collection: what each paid in the last 30 complete days, its 14-day trend and its week (`week` in `src/logic/customers-context.ts`). Those that have left are counted in nothing but the ladder's "left".
69
69
  - **The ladder's moves** are of the last 30 complete days: joined (`joined`), moved up (`planSince` with a `previousPlan` below the plan) and left (`left`).
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.
@@ -80,7 +80,7 @@ Revenue over 30 days equals Home's and the customers' revenue to the cent. Nothi
80
80
 
81
81
  ## Agents
82
82
 
83
- The page itself takes no agent action. It shares one context with both agents (decision 0011), built by `revenueContext` in `src/views/revenue-context.ts` from the figures the page draws; the wall's order is the person's, and the context states the wall in that order.
83
+ The page itself takes no agent action. It shares one context with both agents (decision 0011), built by `revenueContext` in `src/logic/revenue-context.ts` from the figures the page draws; the wall's order is the person's, and the context states the wall in that order.
84
84
 
85
85
  ### What the agent reads
86
86
 
@@ -108,7 +108,7 @@ Protagonist: Revenue.
108
108
  - "Which model brought the most revenue in the last 90 days, and how did that move?" — the usage's revenue over 90 complete days, by model, against the 90 days before
109
109
  - "What do the customers past due pay, by plan?" — the customers whose status is past due, their revenue over the last 30 complete days, by plan
110
110
 
111
- Revenue's prepared query (`revenueExplore` in `src/views/revenue-explore.ts`, set as `explore` on the Revenue tool) reads three sources. `days` is windowed by complete days and bucketed by day, week, month or weekday, with revenue and each plan's revenue. `customers` has no date: each customer's figures cover the last 30 complete days, narrowed and grouped by plan, status, region and owner. `usage` is the days by model, with revenue and requests. A drilled day or model-day opens the page at every day kept, and a drilled customer its own page.
111
+ Revenue's prepared query (`revenueExplore` in `src/logic/revenue-explore.ts`, set as `explore` on the Revenue tool) reads three sources. `days` is windowed by complete days and bucketed by day, week, month or weekday, with revenue and each plan's revenue. `customers` has no date: each customer's figures cover the last 30 complete days, narrowed and grouped by plan, status, region and owner. `usage` is the days by model, with revenue and requests. A drilled day or model-day opens the page at every day kept, and a drilled customer its own page.
112
112
 
113
113
  ## Accessibility
114
114
 
@@ -1,6 +1,7 @@
1
1
  import { Suspense, cache } from 'react';
2
2
  import { app, workspaces } from '@/app.config';
3
3
  import { gate, gatedMetadata } from '@/data/access';
4
+ import { RefusalScreen } from '@/views/refusal';
4
5
  import { DEMO_NOW, DEMO_UPDATED_AT } from '@/data/sample';
5
6
  import { PageFrame } from '@/components/base/shell';
6
7
  import { ExportButton } from '@/components/patterns/export-button';
@@ -9,9 +10,9 @@ import { Busy, LeadRoom } from '@/components/patterns/page-loading';
9
10
  import { PeriodSelect } from '@/components/patterns/period-select';
10
11
  import { parsePeriod } from '@/lib/period';
11
12
  import { PeriodActionsRoom, PeriodRoom } from '@/views/period-actions';
12
- import { dayRows } from '@/views/home-context';
13
+ import { dayRows } from '@/logic/home-context';
13
14
  import { RevenueBody } from '@/views/revenue';
14
- import { revenueTool } from '@/views/tools';
15
+ import { revenueTool } from '@/logic/tools';
15
16
  import { RevenueSkeleton } from './loading';
16
17
 
17
18
  export function generateMetadata() {
@@ -20,7 +21,7 @@ export function generateMetadata() {
20
21
 
21
22
  type SearchParams = Promise<{ period?: string }>;
22
23
 
23
- /** The page's one read, through the Revenue tool (`src/views/tools.ts`), shared by the title, the line, the export and the body. */
24
+ /** The page's one read, through the Revenue tool (`src/logic/tools.ts`), shared by the title, the line, the export and the body. */
24
25
  const readPeriod = cache((period: ReturnType<typeof parsePeriod>) => revenueTool.read({ period }));
25
26
 
26
27
  /**
@@ -32,7 +33,7 @@ const readPeriod = cache((period: ReturnType<typeof parsePeriod>) => revenueTool
32
33
  */
33
34
  export default async function RevenuePage({ searchParams }: { searchParams: SearchParams } = { searchParams: Promise.resolve({}) }) {
34
35
  const denied = await gate('revenue');
35
- if (denied) return denied;
36
+ if (denied) return RefusalScreen(denied);
36
37
  return (
37
38
  <PageFrame
38
39
  kicker={<>{app.name} · {app.workspace}</>}
@@ -30,7 +30,7 @@ The body (rows 1 and 2) is the records template, `Records` from `src/components/
30
30
 
31
31
  The `customers` collection, read through `readAll()` under `customers:read`, like every other collection; its rows are the sample's `CUSTOMERS` (made in `src/system/fixtures/company.ts`): 329 records, 320 that have not left and 9 that have. It is read-only: nobody in the console creates or changes a customer. Revenue is what a customer was billed for metered usage, before credits, over the last 30 complete days; over those days the customers' revenue and requests are the days' own, to the cent and to the request.
32
32
 
33
- The page reads every customer and hands its client the fields a row draws (`customerRow` in `src/views/customers-context.ts`): the id, name, plan, status, requests, 14-day trend, error rate, revenue and the day it joined. What only a customer's own page shows, its requests by route among it, stays on the server. `kept()` and `customerFigures()` there decide what is counted: every customer that has not left.
33
+ The page reads every customer and hands its client the fields a row draws (`customerRow` in `src/logic/customers-context.ts`): the id, name, plan, status, requests, 14-day trend, error rate, revenue and the day it joined. What only a customer's own page shows, its requests by route among it, stays on the server. `kept()` and `customerFigures()` there decide what is counted: every customer that has not left.
34
34
 
35
35
  The page renders the masthead (title, sentence) itself, outside the boundary that reads the address, so a cold load has them in the first HTML. The tiles and the table wait inside their own boundary.
36
36
 
@@ -44,7 +44,7 @@ None yet: a `zz_meridian_customer { id }` view would show one customer's card wi
44
44
 
45
45
  ## Agents
46
46
 
47
- An agent reads customers through this page's shared context (the `customers` view tool reads every customer, as the page does, and takes the same `q`, `plan` and `status`, `left` among them) and their requests view; it changes nothing here: no one in the console creates or changes a customer. The context is built by `customersContext` in `src/views/customers-context.ts`, and the tiles' `hint`s come from its `CUSTOMER_METRICS`.
47
+ An agent reads customers through this page's shared context (the `customers` view tool reads every customer, as the page does, and takes the same `q`, `plan` and `status`, `left` among them) and their requests view; it changes nothing here: no one in the console creates or changes a customer. The context is built by `customersContext` in `src/logic/customers-context.ts`, and the tiles' `hint`s come from its `CUSTOMER_METRICS`.
48
48
 
49
49
  ### What the agent reads
50
50
 
@@ -72,7 +72,7 @@ The page marks the list `data-protagonist="Customers"` on a wrapper whose displa
72
72
  - "Which region are our customers based in, and how many requests do they send?" — the customers' requests over the last 30 complete days, by each customer's own region
73
73
  - "Which Enterprise customers are on a trial or past due, and where do I open them?" — the customers on Enterprise whose status is trial or past due, with up to 20 of them, each with its page's address
74
74
 
75
- Customers' prepared query (`customersExplore` in `src/views/customers-explore.ts`) reads one source, `customers`, which has no date: each customer's figures cover the last 30 complete days. It is narrowed and grouped by plan, status, region and owner, with the customers that have not left, their revenue, requests and tokens. A drilled customer opens its own page, and a call narrowed to one plan or one status opens the list filtered to it.
75
+ Customers' prepared query (`customersExplore` in `src/logic/customers-explore.ts`) reads one source, `customers`, which has no date: each customer's figures cover the last 30 complete days. It is narrowed and grouped by plan, status, region and owner, with the customers that have not left, their revenue, requests and tokens. A drilled customer opens its own page, and a call narrowed to one plan or one status opens the list filtered to it.
76
76
 
77
77
  ## Content
78
78
 
@@ -18,7 +18,7 @@ Five parts, in this order. The page has no period control: every part names the
18
18
 
19
19
  ### The title
20
20
 
21
- The page is headed by what the customer's record says, worked out by `customerTitle` in `src/views/customer-context.ts`. The heading reads the customer's name and a colon before it, out of sight, and the top bar shows the name.
21
+ The page is headed by what the customer's record says, worked out by `customerTitle` in `src/logic/customer-context.ts`. The heading reads the customer's name and a colon before it, out of sight, and the top bar shows the name.
22
22
 
23
23
  | When | The title |
24
24
  |---|---|
@@ -49,7 +49,7 @@ A customer is named by its name across the product, and wherever it is, the name
49
49
 
50
50
  ## Data
51
51
 
52
- One row of the `customers` collection through `read()` under `customers:read`, whose key is the id; the product's revenue and error rate over the last 30 complete days (`readDays('30d')`); and the newest five requests of the `requests` collection whose customer is this one's name (`readLatestFrom` in `src/data/requests.ts`). `readCustomer` in `src/views/tools.ts` reads all three, each apart, and is shared by the page and the Customer tool; the page's metadata and its body share one read. Three ids are built ahead of time, the three largest customers; any other renders on its first request. Nothing writes.
52
+ One row of the `customers` collection through `read()` under `customers:read`, whose key is the id; the product's revenue and error rate over the last 30 complete days (`readDays('30d')`); and the newest five requests of the `requests` collection whose customer is this one's name (`readLatestFrom` in `src/data/requests.ts`). `readCustomer` in `src/logic/tools.ts` reads all three, each apart, and is shared by the page and the Customer tool; the page's metadata and its body share one read. Three ids are built ahead of time, the three largest customers; any other renders on its first request. Nothing writes.
53
53
 
54
54
  Its share of revenue is what it paid over the revenue of the same 30 complete days. The busiest of its 14 days is the largest of its trend, dated from the day before the read.
55
55
 
@@ -59,7 +59,7 @@ Its share of revenue is what it paid over the revenue of the same 30 complete da
59
59
 
60
60
  ## Agents
61
61
 
62
- The page itself takes no agent action. It shares one context with both agents (decision 0011), built by `customerContext` in `src/views/customer-context.ts`.
62
+ The page itself takes no agent action. It shares one context with both agents (decision 0011), built by `customerContext` in `src/logic/customer-context.ts`.
63
63
 
64
64
  ### What the agent reads
65
65
 
@@ -85,7 +85,7 @@ The page marks the card `data-protagonist="Where it stands"` on a wrapper whose
85
85
  - "Which of its sampled requests failed, and where do I open them?" — its requests in the sampled log whose status class is 4xx or 5xx, with up to 20 of them, each with its request's address
86
86
  - "How many tokens did it use in the last 30 days?" — the tokens, requests and revenue of its own row of the customers
87
87
 
88
- The protagonist is the card the structure calls Where it stands, which is titled by where the customer stands and since when. One customer's prepared query (`customerExplore` in `src/views/customer-explore.ts`, set as `explore` on the Customer tool) takes the page's `id` beside the call and reads two sources, neither with a date: its requests in the sampled log, grouped by route or status class, and its own row of the customers. Its `fixed` reads that row by the id under the person's rights and holds every call to the customer's name and id, so no other customer is read; an id that names no customer is answered that there is no such customer, and nothing else is read.
88
+ The protagonist is the card the structure calls Where it stands, which is titled by where the customer stands and since when. One customer's prepared query (`customerExplore` in `src/logic/customer-explore.ts`, set as `explore` on the Customer tool) takes the page's `id` beside the call and reads two sources, neither with a date: its requests in the sampled log, grouped by route or status class, and its own row of the customers. Its `fixed` reads that row by the id under the person's rights and holds every call to the customer's name and id, so no other customer is read; an id that names no customer is answered that there is no such customer, and nothing else is read.
89
89
 
90
90
  ## Accessibility
91
91
 
@@ -1,10 +1,11 @@
1
1
  import { cache } from 'react';
2
2
  import { gate, gatedMetadata, may, Unauthenticated } from '@/data/access';
3
+ import { RefusalScreen } from '@/views/refusal';
3
4
  import { FEATURES } from '@/data/features';
4
5
  import { CustomerView } from '@/views/customer';
5
6
  import { MissingPage } from '@/views/missing-page';
6
7
  import { NOT_FOUND } from '@/views/not-found';
7
- import { readCustomer } from '@/views/tools';
8
+ import { readCustomer } from '@/logic/tools';
8
9
 
9
10
  /** The page's one read, shared by its metadata and its body. */
10
11
  const readOne = cache(readCustomer);
@@ -35,7 +36,7 @@ export async function generateMetadata({ params }: { params: Promise<{ id: strin
35
36
  export default async function CustomerPage({ params }: { params: Promise<{ id: string }> }) {
36
37
  const { id } = await params;
37
38
  const denied = await gate('customer');
38
- if (denied) return denied;
39
+ if (denied) return RefusalScreen(denied);
39
40
  const found = await readOne(id);
40
41
  // Rendered here rather than thrown, so the screen is in the first HTML (see MissingPage).
41
42
  if (!found) return <MissingPage />;
@@ -2,10 +2,11 @@ import { Suspense } from 'react';
2
2
  import { app } from '@/app.config';
3
3
  import { PageFrame } from '@/components/base/shell';
4
4
  import { gate, gatedMetadata } from '@/data/access';
5
+ import { RefusalScreen } from '@/views/refusal';
5
6
  import { readAll } from '@/data/read';
6
7
  import type { CustomerRecord } from '@/data/sample';
7
8
  import { CustomersView } from '@/views/customers';
8
- import { customerRow } from '@/views/customers-context';
9
+ import { customerRow } from '@/logic/customers-context';
9
10
  import { Busy } from '@/components/patterns/page-loading';
10
11
  import { RecordsSkeleton } from '@/components/templates/records';
11
12
 
@@ -20,7 +21,7 @@ export function generateMetadata() {
20
21
  */
21
22
  export default async function CustomersPage() {
22
23
  const denied = await gate('customers');
23
- if (denied) return denied;
24
+ if (denied) return RefusalScreen(denied);
24
25
  return (
25
26
  <PageFrame
26
27
  kicker={<>{app.name} · {app.workspace}</>}