zz-meridian 0.36.0 → 0.37.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 (314) hide show
  1. package/README.md +26 -4
  2. package/dist/adopt.js +39 -27
  3. package/dist/cli.js +14 -2
  4. package/dist/create.js +1 -1
  5. package/dist/files.js +22 -5
  6. package/dist/migrations.js +104 -32
  7. package/dist/reconcile.js +5 -0
  8. package/dist/session.js +2 -2
  9. package/dist/update-session.js +11 -4
  10. package/package.json +1 -1
  11. package/payload/CHANGELOG.md +121 -0
  12. package/payload/app/(dashboard)/(home)/README.md +2 -0
  13. package/payload/app/(dashboard)/(home)/page.tsx +1 -1
  14. package/payload/app/(dashboard)/business/page.tsx +1 -1
  15. package/payload/app/(dashboard)/error.tsx +4 -3
  16. package/payload/app/(dashboard)/layout.tsx +4 -4
  17. package/payload/app/(dashboard)/operations-tickets/README.md +1 -0
  18. package/payload/app/(dashboard)/requests/[id]/README.md +9 -1
  19. package/payload/app/(dashboard)/requests/[id]/actions.ts +7 -2
  20. package/payload/app/(dashboard)/settings/README.md +2 -0
  21. package/payload/app/(dashboard)/settings/actions.ts +1 -1
  22. package/payload/app/(dashboard)/usage/page.tsx +1 -1
  23. package/payload/app/api/export/requests/route.ts +2 -2
  24. package/payload/app/embed/now/README.md +1 -0
  25. package/payload/app/embed/now/view.tsx +3 -2
  26. package/payload/app/embed/requests/README.md +1 -1
  27. package/payload/app/embed/requests/view.tsx +1 -0
  28. package/payload/app/layout.tsx +5 -1
  29. package/payload/app/sign-in/README.md +4 -1
  30. package/payload/app/sign-in/actions.ts +1 -1
  31. package/payload/app/sign-in/panel.tsx +15 -1
  32. package/payload/app/system/(atlas)/[section]/[id]/page.tsx +8 -2
  33. package/payload/app/system/(atlas)/_article.tsx +28 -3
  34. package/payload/app/system/preview/[section]/[card]/page.tsx +2 -1
  35. package/payload/app/system/preview/[section]/[card]/stage.tsx +3 -2
  36. package/payload/docs/assistant.md +17 -4
  37. package/payload/docs/atlas-scenario-coverage.md +8 -8
  38. package/payload/docs/data-layers.md +21 -11
  39. package/payload/docs/distribution.md +17 -8
  40. package/payload/docs/start-a-dashboard.md +2 -0
  41. package/payload/docs/surfaces.md +5 -1
  42. package/payload/docs/testing.md +24 -0
  43. package/payload/journeys/workspaces.ts +7 -7
  44. package/payload/package.json +1 -1
  45. package/payload/scripts/audit.ts +7 -6
  46. package/payload/scripts/brand.ts +23 -14
  47. package/payload/scripts/contrast.ts +1 -1
  48. package/payload/scripts/interactions.ts +6 -2
  49. package/payload/scripts/keyboard.ts +8 -5
  50. package/payload/scripts/lib/answered.ts +1 -3
  51. package/payload/scripts/lib/chrome.ts +15 -12
  52. package/payload/scripts/lib/coverage.ts +19 -1
  53. package/payload/scripts/lib/data-url.ts +60 -42
  54. package/payload/scripts/lib/effective-manifest.ts +19 -0
  55. package/payload/scripts/lib/journey.ts +6 -13
  56. package/payload/scripts/lib/layer-rules.ts +17 -4
  57. package/payload/scripts/lib/routes.ts +4 -1
  58. package/payload/scripts/lib/update-session.ts +9 -4
  59. package/payload/scripts/live.ts +24 -13
  60. package/payload/scripts/navigate.ts +1 -1
  61. package/payload/scripts/shot.ts +1 -1
  62. package/payload/scripts/tokens.ts +23 -8
  63. package/payload/scripts/verify.ts +9 -5
  64. package/payload/skills/zz-meridian/references/cache.md +19 -13
  65. package/payload/skills/zz-meridian/references/customize.md +13 -8
  66. package/payload/skills/zz-meridian/references/data-layers.md +21 -11
  67. package/payload/skills/zz-meridian/references/live.md +6 -2
  68. package/payload/skills/zz-meridian/references/update.md +7 -3
  69. package/payload/skills/zz-meridian/references/validation.md +3 -1
  70. package/payload/src/components/base/providers.tsx +5 -2
  71. package/payload/src/components/base/shell/README.md +3 -1
  72. package/payload/src/components/base/shell/index.tsx +9 -4
  73. package/payload/src/components/base/shell/leave-question.tsx +1 -1
  74. package/payload/src/components/base/shell/preview.tsx +4 -2
  75. package/payload/src/components/base/surface/index.tsx +14 -5
  76. package/payload/src/components/charts/meridian/README.md +2 -1
  77. package/payload/src/components/charts/meridian/index.tsx +15 -3
  78. package/payload/src/components/patterns/activity-feed/README.md +1 -1
  79. package/payload/src/components/patterns/activity-feed/index.tsx +2 -1
  80. package/payload/src/components/patterns/appearance-menu/README.md +4 -4
  81. package/payload/src/components/patterns/appearance-menu/index.tsx +7 -17
  82. package/payload/src/components/patterns/appearance-menu/preview.tsx +10 -9
  83. package/payload/src/components/patterns/ask-about/index.tsx +5 -4
  84. package/payload/src/components/patterns/assistant/index.tsx +8 -5
  85. package/payload/src/components/patterns/assistant/preview.tsx +4 -3
  86. package/payload/src/components/patterns/command-palette/README.md +7 -1
  87. package/payload/src/components/patterns/command-palette/index.tsx +4 -19
  88. package/payload/src/components/patterns/command-palette/open.ts +36 -0
  89. package/payload/src/components/patterns/data-table/README.md +1 -0
  90. package/payload/src/components/patterns/data-table/index.tsx +11 -7
  91. package/payload/src/components/patterns/export-button/README.md +7 -5
  92. package/payload/src/components/patterns/export-button/index.tsx +10 -5
  93. package/payload/src/components/patterns/export-button/preview.tsx +4 -3
  94. package/payload/src/components/patterns/form-section/README.md +4 -4
  95. package/payload/src/components/patterns/form-section/index.tsx +4 -4
  96. package/payload/src/components/patterns/proposal/README.md +6 -5
  97. package/payload/src/components/patterns/proposal/index.tsx +8 -7
  98. package/payload/src/components/patterns/rail/README.md +2 -2
  99. package/payload/src/components/patterns/rail/index.tsx +1 -1
  100. package/payload/src/components/patterns/shell-tools/index.tsx +1 -1
  101. package/payload/src/components/ui/avatar/README.md +2 -0
  102. package/payload/src/components/ui/avatar/index.tsx +9 -5
  103. package/payload/src/components/ui/button/README.md +3 -0
  104. package/payload/src/components/ui/button/index.tsx +31 -15
  105. package/payload/src/components/ui/copy-field/README.md +2 -0
  106. package/payload/src/components/ui/copy-field/index.tsx +28 -18
  107. package/payload/src/components/ui/dialog/index.tsx +1 -1
  108. package/payload/src/components/ui/disclosure/README.md +1 -1
  109. package/payload/src/components/ui/key-value/README.md +1 -1
  110. package/payload/src/components/ui/menu/README.md +1 -0
  111. package/payload/src/components/ui/menu/index.tsx +1 -1
  112. package/payload/src/components/ui/pagination/README.md +2 -2
  113. package/payload/src/components/ui/pagination/index.tsx +3 -2
  114. package/payload/src/components/ui/progress/README.md +1 -1
  115. package/payload/src/components/ui/progress/index.tsx +2 -2
  116. package/payload/src/components/ui/search-input/README.md +2 -0
  117. package/payload/src/components/ui/search-input/index.tsx +3 -2
  118. package/payload/src/components/ui/segmented/README.md +1 -1
  119. package/payload/src/components/ui/segmented/index.tsx +1 -1
  120. package/payload/src/data/README.md +2 -0
  121. package/payload/src/data/access.ts +1 -2
  122. package/payload/src/data/derived.ts +15 -13
  123. package/payload/src/data/read.ts +8 -5
  124. package/payload/src/lib/assistant/prompt.ts +10 -7
  125. package/payload/src/lib/assistant/tools.ts +35 -19
  126. package/payload/src/lib/collection.ts +1 -1
  127. package/payload/src/lib/csv.ts +8 -2
  128. package/payload/src/lib/live.ts +2 -1
  129. package/payload/src/logic/home-context.ts +3 -3
  130. package/payload/src/logic/home-parts.ts +21 -20
  131. package/payload/src/logic/request-context.ts +2 -2
  132. package/payload/src/logic/revenue-context.ts +9 -5
  133. package/payload/src/logic/revenue-parts.ts +10 -8
  134. package/payload/src/styles/base.css +20 -4
  135. package/payload/src/styles/fonts/Geist-Variable.woff2 +0 -0
  136. package/payload/src/styles/fonts/GeistMono-Variable.woff2 +0 -0
  137. package/payload/src/styles/tokens.css +35 -5
  138. package/payload/src/system/atlas-shell.tsx +4 -3
  139. package/payload/src/system/card-stage.tsx +2 -1
  140. package/payload/src/system/fixtures/sample-records.ts +6 -6
  141. package/payload/src/system/preview-loading.tsx +7 -0
  142. package/payload/src/system/sample-cells.tsx +13 -3
  143. package/payload/src/views/console-chrome.tsx +6 -1
  144. package/payload/src/views/console-live.tsx +3 -2
  145. package/payload/src/views/home.tsx +1 -1
  146. package/payload/src/views/members.tsx +16 -10
  147. package/payload/src/views/not-found-address.tsx +1 -1
  148. package/payload/src/views/period-actions.tsx +1 -1
  149. package/payload/src/views/request.tsx +17 -12
  150. package/payload/src/views/revenue.tsx +8 -8
  151. package/payload/src/views/settings.tsx +3 -3
  152. package/payload/src/views/tickets.tsx +17 -3
  153. package/payload/tests/activity-feed-link.test.tsx +1 -2
  154. package/payload/tests/agent-page-spec.test.ts +2 -3
  155. package/payload/tests/agents.test.tsx +0 -11
  156. package/payload/tests/appearance-menu-keyboard.test.tsx +29 -0
  157. package/payload/tests/assistant-access.test.ts +0 -14
  158. package/payload/tests/assistant-column-draft.test.tsx +8 -1
  159. package/payload/tests/assistant-explore-tool.test.ts +3 -19
  160. package/payload/tests/assistant-guard.test.ts +114 -1
  161. package/payload/tests/assistant-prompt-truth.test.ts +5 -11
  162. package/payload/tests/assistant-proposal.test.tsx +39 -4
  163. package/payload/tests/assistant-questions.test.tsx +0 -1
  164. package/payload/tests/assistant-safety.test.ts +80 -2
  165. package/payload/tests/assistant-thread.test.tsx +0 -1
  166. package/payload/tests/assistant-tools.test.ts +2 -4
  167. package/payload/tests/assistant-withheld.test.ts +1 -10
  168. package/payload/tests/atlas-layers.test.ts +1 -40
  169. package/payload/tests/atlas-product-slice.test.tsx +1 -26
  170. package/payload/tests/atlas-shell-early-press.test.tsx +24 -0
  171. package/payload/tests/atlas-template-contracts.test.tsx +2 -69
  172. package/payload/tests/avatar-initials.test.tsx +11 -0
  173. package/payload/tests/banner-preview.test.tsx +0 -13
  174. package/payload/tests/bar-list-forms.test.tsx +2 -8
  175. package/payload/tests/brand-config.test.tsx +1 -4
  176. package/payload/tests/button-busy.test.tsx +45 -0
  177. package/payload/tests/card-header-title.test.tsx +0 -14
  178. package/payload/tests/card-stage-loading.test.tsx +40 -0
  179. package/payload/tests/check-nav-under.test.ts +0 -7
  180. package/payload/tests/chrome-navigation.test.tsx +1 -7
  181. package/payload/tests/client-imports.test.ts +0 -8
  182. package/payload/tests/column-chart-crowded.test.tsx +0 -7
  183. package/payload/tests/column-chart-grouped.test.tsx +4 -20
  184. package/payload/tests/column-chart-labels.test.tsx +4 -42
  185. package/payload/tests/column-chart-no-data.test.tsx +0 -4
  186. package/payload/tests/column-chart-readout.test.tsx +0 -3
  187. package/payload/tests/column-chart-running.test.tsx +0 -21
  188. package/payload/tests/column-chart-signed.test.tsx +1 -4
  189. package/payload/tests/column-chart.test.tsx +2 -8
  190. package/payload/tests/composition-bar.test.tsx +0 -8
  191. package/payload/tests/console-live.test.tsx +4 -0
  192. package/payload/tests/copy-field.test.tsx +131 -1
  193. package/payload/tests/counts-and-lists.test.tsx +1 -2
  194. package/payload/tests/csv-bom.test.ts +27 -0
  195. package/payload/tests/customer-figures.test.ts +0 -13
  196. package/payload/tests/customers-masthead.test.tsx +0 -1
  197. package/payload/tests/data-read.test.ts +3 -4
  198. package/payload/tests/data-table.test.tsx +8 -9
  199. package/payload/tests/day-in-progress.test.ts +0 -1
  200. package/payload/tests/derivations.test.ts +0 -27
  201. package/payload/tests/derived-answer.test.ts +178 -19
  202. package/payload/tests/disclosure.test.tsx +2 -3
  203. package/payload/tests/diverging-bars.test.tsx +1 -6
  204. package/payload/tests/effective-manifest.test.ts +56 -0
  205. package/payload/tests/embed-context-lifetime.test.tsx +79 -0
  206. package/payload/tests/embed-page-failure.test.tsx +1 -8
  207. package/payload/tests/embed-requests-provenance.test.tsx +25 -1
  208. package/payload/tests/embed-theme.test.tsx +62 -0
  209. package/payload/tests/engineering-workflows.test.ts +4 -53
  210. package/payload/tests/explore-answer-length.test.ts +0 -16
  211. package/payload/tests/export-button.test.tsx +45 -0
  212. package/payload/tests/facts-engineering.test.ts +1 -12
  213. package/payload/tests/facts.test.ts +1 -18
  214. package/payload/tests/feature-access.test.ts +1 -7
  215. package/payload/tests/feature-definitions.test.ts +1 -21
  216. package/payload/tests/featured-metric.test.tsx +1 -24
  217. package/payload/tests/filter-bar-search.test.tsx +0 -18
  218. package/payload/tests/form-section-actions.test.tsx +0 -3
  219. package/payload/tests/format-semantics.test.ts +1 -3
  220. package/payload/tests/heatmap-states.test.tsx +1 -37
  221. package/payload/tests/honest-controls.test.tsx +0 -4
  222. package/payload/tests/incident-card-hyphens.test.tsx +3 -18
  223. package/payload/tests/input-states.test.tsx +0 -4
  224. package/payload/tests/key-value.test.tsx +2 -36
  225. package/payload/tests/linked-records.test.ts +1 -2
  226. package/payload/tests/live-client.test.tsx +65 -0
  227. package/payload/tests/loading-routes.test.tsx +1 -10
  228. package/payload/tests/member-mutations.test.ts +2 -5
  229. package/payload/tests/member-roles.test.ts +0 -11
  230. package/payload/tests/members-optimistic.test.tsx +13 -23
  231. package/payload/tests/meridian.test.tsx +25 -2
  232. package/payload/tests/metric-tile-compact.test.tsx +0 -38
  233. package/payload/tests/metric-tile-delta-format.test.tsx +1 -2
  234. package/payload/tests/metric-tile-note.test.tsx +1 -15
  235. package/payload/tests/metric-tile-title.test.tsx +0 -1
  236. package/payload/tests/metric-tile.test.tsx +2 -17
  237. package/payload/tests/no-access.test.tsx +1 -3
  238. package/payload/tests/page-frame-title.test.tsx +0 -92
  239. package/payload/tests/page-loading.test.tsx +65 -44
  240. package/payload/tests/page-server-module.test.ts +10 -1
  241. package/payload/tests/pagination-sizes.test.tsx +16 -0
  242. package/payload/tests/palette-hint.test.tsx +0 -8
  243. package/payload/tests/palette-open.test.ts +24 -0
  244. package/payload/tests/popover-name.test.tsx +1 -2
  245. package/payload/tests/progress-values.test.tsx +5 -3
  246. package/payload/tests/proposal-approval.test.tsx +31 -0
  247. package/payload/tests/prose-cell-labels.test.tsx +0 -8
  248. package/payload/tests/rail-branch-feel.test.tsx +3 -71
  249. package/payload/tests/rail-branch.test.tsx +17 -23
  250. package/payload/tests/read-all-query.test.ts +57 -7
  251. package/payload/tests/records-consumers.test.tsx +9 -77
  252. package/payload/tests/refusal-descriptor.test.ts +0 -11
  253. package/payload/tests/release-phone-cell.test.ts +0 -3
  254. package/payload/tests/release-wording.test.ts +2 -7
  255. package/payload/tests/request-curl.test.ts +97 -16
  256. package/payload/tests/request-replay.test.ts +12 -1
  257. package/payload/tests/request-usage.test.tsx +2 -2
  258. package/payload/tests/request-view-replay.test.tsx +36 -28
  259. package/payload/tests/review-held-scales.test.tsx +1 -32
  260. package/payload/tests/roles-context.test.ts +0 -1
  261. package/payload/tests/roles-matrix.test.tsx +0 -2
  262. package/payload/tests/script-args.test.ts +8 -0
  263. package/payload/tests/search-input.test.tsx +54 -0
  264. package/payload/tests/segmented-measure.test.tsx +23 -0
  265. package/payload/tests/settings-access.test.tsx +1 -4
  266. package/payload/tests/settings-agents.test.tsx +0 -18
  267. package/payload/tests/settings-save.test.tsx +1 -15
  268. package/payload/tests/shared-context.test.tsx +35 -3
  269. package/payload/tests/shell-alerts.test.ts +0 -10
  270. package/payload/tests/shell-promise.test.tsx +1 -8
  271. package/payload/tests/shell-search-name.test.tsx +0 -13
  272. package/payload/tests/sign-in-honest.test.tsx +19 -11
  273. package/payload/tests/sign-in-persona.test.ts +14 -1
  274. package/payload/tests/table-cut-cell.test.tsx +13 -40
  275. package/payload/tests/table-grid.test.tsx +2 -57
  276. package/payload/tests/template-consumers.test.tsx +0 -4
  277. package/payload/tests/tickets-view.test.tsx +53 -4
  278. package/payload/tests/tile-row.test.tsx +29 -0
  279. package/payload/tests/trend-chart-band.test.tsx +1 -3
  280. package/payload/tests/trend-chart-forms.test.tsx +1 -2
  281. package/payload/tests/update-session.test.ts +19 -2
  282. package/payload/tests/uptime-bars-end.test.tsx +1 -1
  283. package/payload/tests/uptime-edges.test.tsx +0 -7
  284. package/payload/tests/usage-figures.test.ts +0 -11
  285. package/payload/tests/verify-coverage.test.ts +10 -0
  286. package/payload/tests/verify-data-url.test.ts +76 -15
  287. package/payload/tests/verify-explore-surfaces.test.ts +10 -7
  288. package/payload/tests/verify-loopback.test.ts +0 -11
  289. package/payload/tests/verify-personas.test.ts +0 -21
  290. package/payload/tests/verify-sample-surfaces.test.ts +0 -4
  291. package/payload/tests/verify-sizes.test.ts +1 -5
  292. package/payload/tests/verify-suite-outcome.test.ts +12 -1
  293. package/payload/tests/verify-timing.test.ts +1 -2
  294. package/payload/tests/view-as.test.ts +1 -6
  295. package/payload/tests/view-tool-modules.test.ts +1 -2
  296. package/payload/tests/view-tool-needs.test.ts +0 -11
  297. package/payload/tests/workspace-access.test.ts +20 -3
  298. package/payload/tests/workspace-chrome.test.tsx +1 -13
  299. package/payload/tests/workspace-data-paths.test.ts +0 -13
  300. package/payload/tests/workspace-home.test.tsx +0 -9
  301. package/payload/tests/workspaces.test.ts +1 -27
  302. package/payload/tests/bar-list-axis-words.test.tsx +0 -42
  303. package/payload/tests/dialog-xl.test.tsx +0 -25
  304. package/payload/tests/error-retry.test.tsx +0 -26
  305. package/payload/tests/feel-bar-list.test.tsx +0 -22
  306. package/payload/tests/filter-bar-view.test.tsx +0 -20
  307. package/payload/tests/forced-colors-states.test.ts +0 -62
  308. package/payload/tests/hatch.test.ts +0 -45
  309. package/payload/tests/heatmap-names-column.test.tsx +0 -30
  310. package/payload/tests/segmented-fit.test.tsx +0 -22
  311. package/payload/tests/select-touch.test.tsx +0 -13
  312. package/payload/tests/table-cut-align.test.tsx +0 -30
  313. package/payload/tests/table-hide-below-pad.test.tsx +0 -18
  314. package/payload/tests/tabs-indicator.test.tsx +0 -18
package/README.md CHANGED
@@ -37,17 +37,26 @@ npx zz-meridian@latest skill [--global] # install only the agent skill
37
37
  Brand flags: `--name "Acme Ops"`, `--hex '#2E6BE4'` (or `--hue 250 --chroma 0.14`, or `--accent indigo|cobalt|jade|graphite`), `--theme dark|light`,
38
38
  `--workspace`, `--timezone`, `--currency`, `--user`, `--role`. `--workspace` sets `app.workspace`, the environment named
39
39
  in a page's kicker ("Production"); it does not touch the navigation workspaces.
40
+ `--timezone` must be accepted by `Intl.DateTimeFormat`, for example `UTC` or `Europe/London`; an unsupported zone is refused rather than saved.
41
+ Quoted brand strings remain literal values across repeated rebranding; changing a role does not rewrite the person's existing name.
42
+ Every valued flag needs its own value, including with the standalone `pnpm brand`: missing values and a following flag used as a value are refused before any configuration changes.
43
+ Commands reject extra positional arguments before changing files: only `create` takes a folder argument.
40
44
 
41
45
  **adopt** copies Meridian's tokens, styles, components, gates and scripts into `src/`, `tokens/` and `scripts/`;
42
46
  merges the dependencies and scripts it needs into your `package.json`; adds the `@meridian/*` alias to your
43
47
  `tsconfig.json` (yours is kept as `tsconfig.before.json` when it carries comments or trailing commas, which JSON
44
48
  cannot hold); replaces your global stylesheet (yours is kept
45
- beside it as `*.before.css`); brands it; adds Meridian's managed block to your `AGENTS.md` between two markers, keeping your own text there byte
49
+ beside it as `*.before.css`); preserves string keys and values in your TypeScript configuration, including punctuation;
50
+ brands it; adds Meridian's managed block to your `AGENTS.md` between two markers, keeping your own text there byte
46
51
  for byte; writes an empty `docs/brief.md` (your product, users, data, decisions and glossary) if you have none; installs the skill into
47
52
  `.agents/skills/` (Codex) and `.claude/skills/` (Claude Code); records every Meridian file it copied in `.meridian/manifest.json`;
48
- installs and type checks. Your routes, your data layer and your own components are not touched. Meridian's files
49
- import each other by relative path, so a `components/ui/button` of your own is never confused with Meridian's; your
50
- code imports Meridian as `@meridian/…`.
53
+ installs and reports route-generation and TypeScript diagnostics. An installation failure exits nonzero; diagnostic
54
+ failures are reported as `types: fails` but adoption still exits zero, because your existing routes may need repairs.
55
+ This is not a clean-typecheck guarantee: fix those diagnostics before running the gate. Your routes, your data layer
56
+ and your own components are not touched. Meridian's files import each other by relative path, so a
57
+ `components/ui/button` of your own is never confused with Meridian's; your code imports Meridian as `@meridian/…`.
58
+ Supported dependency specifications below Meridian's required floor are raised, including within the same major;
59
+ newer versions are kept. Custom specifications are kept with a note to confirm compatibility.
51
60
 
52
61
  It refuses, and writes nothing, when the git tree has uncommitted changes (so its change is one reviewable diff), when
53
62
  the project is not Next.js with the App Router, or when a file it would copy already exists with other content.
@@ -68,10 +77,12 @@ writes nothing; run it first.
68
77
  - It reports a migration for each interface the release changed where one of your own files still has the old shape,
69
78
  such as `cache-components-config` or `nav-needs`. `cli/src/migrations.ts` holds every one; resolve them as the skill's
70
79
  `references/update.md` says.
80
+ - Team-owned sample files are checked again on every forward upgrade, even after their migration's introduction, so newer managed tests do not silently outrun the sample they exercise. They remain team-owned: the migration asks for review and never overwrites them.
71
81
  - Everything it did and everything left to do is in `.meridian/update/<version>/MERGE.md`. Resolve the items, then run
72
82
  the pinned `npx zz-meridian@<version> update --finalize`; `--resume` continues an interrupted run and `--abort` restores
73
83
  what it changed. Only finalize records the new version. `--finalize --verify` validates with the project's default
74
84
  `verify` instead of the gate and the build, and reports its coverage line.
85
+ - Failure and coverage metadata is recorded literally in the report, including `$` sequences; it is not interpreted as report-template syntax.
75
86
  - It refuses, writing nothing, on a dirty git tree (unless `--allow-dirty`) or while another update is open. `--verbose`
76
87
  lists every file.
77
88
 
@@ -80,6 +91,9 @@ styles) and `src/app.config.ts` for the new flags and records them in the manife
80
91
  refuses when one of those outputs was edited, while an update is open, or on a dirty tree (unless `--allow-dirty`). Run
81
92
  it with the version the manifest records. The project's own `pnpm brand` still works, but its changes count as your edits.
82
93
 
94
+ **skill** checks both `.agents/skills/zz-meridian` and `.claude/skills/zz-meridian` before writing either. Identical files are retained; different content or symlinks are refused. Move a customized copy aside, install the shipped skill, then reconcile the customization manually rather than losing it to an overwrite.
95
+ An incomplete package without its `SKILL.md` is refused before either destination is written.
96
+
83
97
  The standard a project on Meridian is built to is
84
98
  [data-layers.md](https://github.com/zhixuan312/zz-meridian/blob/master/docs/data-layers.md): the four layers between a
85
99
  database and the screen, the contract a backend answers, and the six rules a project switches on in its own
@@ -102,6 +116,14 @@ telemetry. It talks to npm only, when `npx` fetches it and when `update` reads r
102
116
  public, so leave out anything that identifies you, your organisation or your product, and describe the problem with
103
117
  Meridian's own template or sample data.
104
118
 
119
+ ## Maintaining this package
120
+
121
+ From the repository root, run `node cli/scripts/build-payload.ts` before `pnpm exec tsc -p cli/tsconfig.json` and packing. The producer reads committed `HEAD` by default; use `--worktree` to try uncommitted tracked changes. It also generates `cli/src/update-session.ts` from the canonical standalone `scripts/lib/update-session.ts`, using the same source provenance as the payload. Edit only the canonical module, never its generated CLI companion.
122
+ The payload excludes `.env` and its variants at every directory depth, even when tracked; deliberate `.env.example`
123
+ files remain available as public configuration templates. Never put credentials in an example file.
124
+
125
+ The template's copied-command regression runs real cURL on loopback and needs a POSIX shell and `curl`, with no external endpoint or credentials.
126
+
105
127
  ## License
106
128
 
107
129
  MIT: the text is `payload/LICENSE` in the package and `LICENSE` in a project `create` makes.
package/dist/adopt.js CHANGED
@@ -12,9 +12,41 @@ import path from 'node:path';
12
12
  import { VERSION, brandArgs, effectiveBrand, inside, installArgs, installSkill, jsoncExtras, packageManager, payloadBytes, payloadFiles, readJsonc, readPayload, relativeImports, skillPaths, run, runTool, sha256, writeIn, writeManifest, } from './files.js';
13
13
  import { BRIEF_TEMPLATE, managedBlock, upsertManagedBlock } from './context.js';
14
14
  import { FIXED, adoptSetOf, managedPaths, parseManifest } from './ownership.js';
15
- import { MERIDIAN_SCRIPTS, TOOLCHAIN, needOf } from './reconcile.js';
16
- const major = (spec) => Number(/(\d+)/.exec(spec)?.[1] ?? NaN);
15
+ import { MERIDIAN_SCRIPTS, TOOLCHAIN, compareMinimum, needOf } from './reconcile.js';
17
16
  const TS = /\.(ts|tsx)$/;
17
+ /** Raise known dependency floors in place; custom specifications stay the team's decision. */
18
+ export function adoptDependencies(pkg, tpl, field, names) {
19
+ const notes = [];
20
+ pkg[field] ??= {};
21
+ for (const n of names) {
22
+ const want = tpl.dependencies?.[n] ?? tpl.devDependencies?.[n];
23
+ if (!want)
24
+ continue;
25
+ const have = pkg.dependencies?.[n] ?? pkg.devDependencies?.[n];
26
+ if (!have) {
27
+ pkg[field][n] = want;
28
+ continue;
29
+ }
30
+ const order = compareMinimum(have, want);
31
+ if (order === null) {
32
+ notes.push(`kept ${n} ${have}, which Meridian cannot compare: confirm it meets ${want} before running the gate`);
33
+ }
34
+ else if (order < 0) {
35
+ const at = pkg.dependencies?.[n] ? 'dependencies' : 'devDependencies';
36
+ pkg[at][n] = want;
37
+ notes.push(`raised ${n} from ${have} to ${want} (Meridian needs this floor; read its upgrade guide)`);
38
+ }
39
+ }
40
+ return notes;
41
+ }
42
+ /** Adoption is installed even when a team's existing routes need type repairs; report both diagnostic stages. */
43
+ function adoptionTypes(root) {
44
+ const route = runTool(root, 'next/dist/bin/next', ['typegen']);
45
+ const tsc = runTool(root, 'typescript/bin/tsc', ['--noEmit']);
46
+ const failures = [['next typegen', route], ['tsc', tsc]];
47
+ const failed = failures.filter(([, result]) => result.status !== 0);
48
+ return failed.length ? `fails:\n${failed.map(([name, result]) => `${name}:\n${(result.stdout + result.stderr).trim().split('\n').slice(0, 30).join('\n')}`).join('\n')}` : 'passes';
49
+ }
18
50
  /** A specifier inside a copied file, pinned to the exact module so a team file of a similar name can never win. */
19
51
  function exactImports(rel, source, known) {
20
52
  const pin = (spec) => {
@@ -267,29 +299,11 @@ Rename or move them, then run adopt again.`);
267
299
  }
268
300
  // ── Merged files: the project's own, changed only where Meridian needs it ────────────────────────────
269
301
  const tpl = JSON.parse(readPayload('package.json'));
270
- const merge = (field, names) => {
271
- pkg[field] ??= {};
272
- for (const n of names) {
273
- const want = tpl.dependencies?.[n] ?? tpl.devDependencies?.[n];
274
- if (!want)
275
- continue;
276
- const have = pkg.dependencies?.[n] ?? pkg.devDependencies?.[n];
277
- if (!have) {
278
- pkg[field][n] = want;
279
- continue;
280
- }
281
- if (major(have) < major(want)) {
282
- const at = pkg.dependencies?.[n] ? 'dependencies' : 'devDependencies';
283
- pkg[at][n] = want;
284
- notes.push(`raised ${n} from ${have} to ${want} (Meridian needs the newer major; read its upgrade guide)`);
285
- }
286
- }
287
- };
288
302
  // The packages the adopted modules import, and the framework: the rule `update` applies to a created project.
289
303
  const sources = new Map(set.filter((rel) => TS.test(rel)).map((rel) => [rel, payloadBytes(rel).toString('utf8')]));
290
304
  sources.set('src/app.config.ts', config);
291
- merge('dependencies', needOf(tpl, sources).runtime);
292
- merge('devDependencies', TOOLCHAIN);
305
+ notes.push(...adoptDependencies(pkg, tpl, 'dependencies', needOf(tpl, sources).runtime));
306
+ notes.push(...adoptDependencies(pkg, tpl, 'devDependencies', TOOLCHAIN));
293
307
  pkg.scripts ??= {};
294
308
  for (const s of MERIDIAN_SCRIPTS) {
295
309
  const want = tpl.scripts[s];
@@ -406,16 +420,14 @@ Rename or move them, then run adopt again.`);
406
420
  files[rel] = sha256(fs.readFileSync(inside(root, rel)));
407
421
  installSkill(root, files);
408
422
  writeManifest(root, { version: VERSION, route: 'adopt', brand, files });
409
- // ── Install and prove it compiles ─────────────────────────────────────────────────────────────────────
423
+ // ── Install and report type diagnostics (existing team code may still need repairs) ──────────────────
410
424
  let typed = 'not run (--no-install)';
411
425
  if (o.install) {
412
426
  say(`\nInstalling with ${pm}…`);
413
427
  if (run(pm, installArgs(pm), root).status !== 0)
414
428
  return fail(`${pm} install failed (above). Fix it, then run ${pm} install and ${pm} run typecheck.`);
415
- // Route types first (LayoutProps, PageProps), as the gate does.
416
- runTool(root, 'next/dist/bin/next', ['typegen']);
417
- const tsc = runTool(root, 'typescript/bin/tsc', ['--noEmit']);
418
- typed = tsc.status === 0 ? 'passes' : `fails:\n${(tsc.stdout + tsc.stderr).trim().split('\n').slice(0, 30).join('\n')}`;
429
+ // Route types first (LayoutProps, PageProps), as the gate does; neither failure may be reported as passing.
430
+ typed = adoptionTypes(root);
419
431
  }
420
432
  say(`
421
433
  Meridian ${VERSION} is in ${root}.
package/dist/cli.js CHANGED
@@ -37,6 +37,7 @@ const HELP = `zz-meridian ${VERSION}
37
37
 
38
38
  Brand flags: --name "Acme Ops" --workspace Production --timezone Europe/London --currency EUR
39
39
  --user "Ada Park" --role Admin --accent indigo|cobalt|jade|graphite | --hex '#2E6BE4' | --hue 250 --chroma 0.14
40
+ --theme dark|light
40
41
 
41
42
  Then give your coding agent the skill: .agents/skills/zz-meridian/SKILL.md.`;
42
43
  const read = readFlags(process.argv.slice(2));
@@ -69,6 +70,11 @@ function main() {
69
70
  console.error(`zz-meridian ${command}: --${wrong.flag} belongs to ${wrong.belongsTo}`);
70
71
  return 1;
71
72
  }
73
+ const extra = positionals[command === 'create' ? 2 : 1];
74
+ if (extra !== undefined) {
75
+ console.error(`zz-meridian ${command}: unexpected argument ${JSON.stringify(extra)}`);
76
+ return 1;
77
+ }
72
78
  if (command === 'adopt')
73
79
  return adopt({ root: process.cwd(), brand, allowDirty: Boolean(flags['allow-dirty']), install });
74
80
  if (command === 'create') {
@@ -95,8 +101,14 @@ function main() {
95
101
  return brandCommand({ root: process.cwd(), flags: brand, allowDirty: Boolean(flags['allow-dirty']) });
96
102
  if (command === 'skill') {
97
103
  const roots = flags.global ? [os.homedir()] : [process.cwd()];
98
- for (const r of roots)
99
- installSkill(r);
104
+ try {
105
+ for (const r of roots)
106
+ installSkill(r);
107
+ }
108
+ catch (error) {
109
+ console.error(`zz-meridian skill: ${error instanceof Error ? error.message : String(error)}`);
110
+ return 1;
111
+ }
100
112
  const skill = path.join(roots[0], '.claude/skills/zz-meridian/SKILL.md');
101
113
  console.log(`The zz-meridian skill is in ${flags.global ? path.join(os.homedir(), '.agents/skills') + ' and ' + path.join(os.homedir(), '.claude/skills') : '.agents/skills and .claude/skills'}. Codex may need a restart to list it.`);
102
114
  console.log(`To follow it now, read ${skill} and the references beside it: it says how to build what was asked and when it is done.`);
package/dist/create.js CHANGED
@@ -73,7 +73,7 @@ export function create(o) {
73
73
  if (hasCommand('git'))
74
74
  run('git', ['init', '-q'], root);
75
75
  if (o.install && run(install[0], install[1], root).status !== 0)
76
- return fail(`${install.join(' ')} failed (above); run it again in ${root}`);
76
+ return fail(`${install[0]} ${install[1].join(' ')} failed (above); run it again in ${root}`);
77
77
  console.log(`
78
78
  ${name} is a new Meridian ${VERSION} dashboard in ${root}.
79
79
  skill: .agents/skills/zz-meridian (Codex) and .claude/skills/zz-meridian (Claude Code)
package/dist/files.js CHANGED
@@ -140,7 +140,8 @@ function strippedJsonc(text) {
140
140
  }
141
141
  out += c;
142
142
  }
143
- const json = out.replace(/,(\s*[}\]])/g, '$1');
143
+ // Consume strings whole before removing trailing commas: their keys and values may themselves contain ",]" or ",}".
144
+ const json = out.replace(/"(?:\\.|[^"\\])*"|,(\s*[}\]])/g, (match, end) => end ?? match);
144
145
  return { json, dropped: dropped || json.length !== out.length };
145
146
  }
146
147
  /** JSON with the comments and trailing commas a tsconfig may carry. */
@@ -184,13 +185,29 @@ export function writeManifest(root, m) {
184
185
  */
185
186
  export function skillPaths() {
186
187
  const files = payloadFiles('skills/zz-meridian');
188
+ if (!files.includes('skills/zz-meridian/SKILL.md'))
189
+ throw new Error('skills/zz-meridian/SKILL.md is not in cli/payload/: rebuild the payload or reinstall a complete package before installing the skill.');
187
190
  return ['.agents/skills/zz-meridian', '.claude/skills/zz-meridian'].flatMap((base) => files.map((f) => ({ rel: `${base}/${f.slice('skills/zz-meridian/'.length)}`, payload: f })));
188
191
  }
189
- /** The agent skill, into the folders each agent reads in a repository. */
192
+ /** Refuse a different existing skill before writing either agent's copy. Identical copies need no write. */
190
193
  export function installSkill(root, record) {
191
- for (const { rel, payload } of skillPaths()) {
192
- const content = payloadBytes(payload);
193
- writeIn(root, rel, content);
194
+ const contents = new Map();
195
+ const plan = skillPaths().map(({ rel, payload }) => {
196
+ const abs = inside(root, rel);
197
+ let content = contents.get(payload);
198
+ if (!content) {
199
+ content = payloadBytes(payload);
200
+ contents.set(payload, content);
201
+ }
202
+ const existing = fs.lstatSync(abs, { throwIfNoEntry: false });
203
+ if (existing && (!existing.isFile() || !fs.readFileSync(abs).equals(content))) {
204
+ throw new Error(`refusing to overwrite ${rel}: it differs from the packaged skill. Move your copy aside before installing again; no skill files were written.`);
205
+ }
206
+ return { rel, content, exists: Boolean(existing) };
207
+ });
208
+ for (const { rel, content, exists } of plan) {
209
+ if (!exists)
210
+ writeIn(root, rel, content);
194
211
  if (record)
195
212
  record[rel] = sha256(content);
196
213
  }
@@ -106,8 +106,16 @@ function removedVerifyFlags(root) {
106
106
  const hits = [...new Set(files)].filter((rel) => removed.test(text(root, rel) ?? ''));
107
107
  return hits.length > 0 ? hits : null;
108
108
  }
109
+ const APP_DIRS = ['app', 'src/app'];
110
+ /** The route's path within either supported App Router folder, or null outside one. */
111
+ const appPath = (rel) => rel.startsWith('app/') ? rel.slice(4) : rel.startsWith('src/app/') ? rel.slice(8) : null;
112
+ const appFiles = (root, rel) => APP_DIRS.flatMap((dir) => {
113
+ const file = `${dir}/${rel}`;
114
+ const src = text(root, file);
115
+ return src === null ? [] : [{ rel: file, src }];
116
+ });
109
117
  const base = (rel) => rel.slice(rel.lastIndexOf('/') + 1);
110
- const isPageOrLayout = (rel) => rel.startsWith('app/') && /^(?:page|layout)\.tsx?$/.test(base(rel));
118
+ const isPageOrLayout = (rel) => appPath(rel) !== null && /^(?:page|layout)\.tsx?$/.test(base(rel));
111
119
  const isActionsFile = (rel) => /^actions\.tsx?$/.test(base(rel));
112
120
  const isServerFile = (src) => /^\s*(['"])use server\1/m.test(src);
113
121
  /** The tag `<name …>` bodies in a file, with `=>` inside an attribute not ending the tag. */
@@ -434,7 +442,7 @@ export const RELEASE_MIGRATIONS = [
434
442
  {
435
443
  id: 'authorized-read',
436
444
  since: '0.5.0',
437
- applies: (root) => matching(root, (rel, src) => rel.startsWith('app/') && /^page\.tsx?$/.test(base(rel)) && /\.query\(/.test(src)),
445
+ applies: (root) => matching(root, (rel, src) => appPath(rel) !== null && /^page\.tsx?$/.test(base(rel)) && /\.query\(/.test(src)),
438
446
  summary: 'A page reads a collection through the authorized, cached read(), not by querying it directly.',
439
447
  instructions: 'Read "Authorized, scoped reads" in references/cache.md. Replace `collection.query(q)` in the page with `read(name, q)` from `src/data/read.ts`, format any freshness against the returned `observedAt`, and add `src/data/access.ts` and `src/data/read.ts` from the reference, bound to your own sign-in and database predicates.',
440
448
  checks: GATE_AND_BUILD,
@@ -452,13 +460,12 @@ export const RELEASE_MIGRATIONS = [
452
460
  since: '0.5.0',
453
461
  applies: (root) => {
454
462
  const out = [];
455
- const layout = 'app/(dashboard)/layout.tsx';
456
463
  const collections = 'src/data/collections.ts';
457
464
  if (text(root, collections) !== null && text(root, 'src/data/live-actions.ts') === null)
458
465
  out.push(collections);
459
- const src = text(root, layout);
460
- if (src !== null && !/\b(?:LiveProvider|ConsoleLive)\b/.test(src))
461
- out.push(layout);
466
+ for (const { rel, src } of appFiles(root, '(dashboard)/layout.tsx'))
467
+ if (!/\b(?:LiveProvider|ConsoleLive)\b/.test(src))
468
+ out.push(rel);
462
469
  return out.length > 0 ? out : null;
463
470
  },
464
471
  summary: 'The console has a live route, a refresh action and a provider.',
@@ -468,7 +475,7 @@ export const RELEASE_MIGRATIONS = [
468
475
  {
469
476
  id: 'authorized-endpoints',
470
477
  since: '0.5.0',
471
- applies: (root) => matching(root, (rel, src) => (rel === 'app/api/assistant/route.ts' || (isActionsFile(rel) && isServerFile(src))) && !/\bresolveAccess\(/.test(src)),
478
+ applies: (root) => matching(root, (rel, src) => (appPath(rel) === 'api/assistant/route.ts' || (isActionsFile(rel) && isServerFile(src))) && !/\bresolveAccess\(/.test(src)),
472
479
  summary: 'The assistant route and every actions file resolve the caller before they act.',
473
480
  instructions: 'Read "Writes authorize, then invalidate" and "The assistant route" in references/cache.md. Call `resolveAccess()` first in the route and in each Server Action, answer 401 or an error result when there is no session, and hand the assistant only the collections the caller may read. The route passes a guard that authorizes each approved change again when it runs, so `src/lib/assistant/tools.ts` and `src/lib/assistant/respond.ts` take that guard too: bring them over from the release with the route.',
474
481
  checks: GATE_AND_BUILD,
@@ -485,13 +492,13 @@ export const RELEASE_MIGRATIONS = [
485
492
  id: 'assistant-view-tools',
486
493
  since: '0.10.0',
487
494
  applies: (root) => {
488
- const route = text(root, 'app/api/assistant/route.ts');
495
+ const routes = appFiles(root, 'api/assistant/route.ts');
489
496
  const tools = text(root, 'src/lib/assistant/tools.ts');
490
497
  const respond = text(root, 'src/lib/assistant/respond.ts');
491
- if (route === null || tools === null || respond === null)
498
+ if (routes.length === 0 || tools === null || respond === null)
492
499
  return null;
493
500
  // The 0.9.0 assistant takes the views and tells the model its limits; one kept from before does neither.
494
- const stale = [...(/\bViewTool\b/.test(tools) ? [] : ['src/lib/assistant/tools.ts']), ...(/\blimitsOf\b/.test(respond) ? [] : ['src/lib/assistant/respond.ts']), ...(/\bviews\s*:/.test(route) ? [] : ['app/api/assistant/route.ts'])];
501
+ const stale = [...(/\bViewTool\b/.test(tools) ? [] : ['src/lib/assistant/tools.ts']), ...(/\blimitsOf\b/.test(respond) ? [] : ['src/lib/assistant/respond.ts']), ...routes.filter(({ src }) => !/\bviews\s*:/.test(src)).map(({ rel }) => rel)];
495
502
  return stale.length > 0 ? stale : null;
496
503
  },
497
504
  summary: "The assistant's route-side code predates view tools and its limits.",
@@ -573,7 +580,10 @@ export const RELEASE_MIGRATIONS = [
573
580
  {
574
581
  id: 'view-page-matched-as-next',
575
582
  since: '0.16.3',
576
- applies: (root) => (/\bfunction onPage\s*\(/.test(text(root, 'app/api/assistant/route.ts') ?? '') ? ['app/api/assistant/route.ts'] : null),
583
+ applies: (root) => {
584
+ const paths = appFiles(root, 'api/assistant/route.ts').filter(({ src }) => /\bfunction onPage\s*\(/.test(src)).map(({ rel }) => rel);
585
+ return paths.length > 0 ? paths : null;
586
+ },
577
587
  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.',
578
588
  instructions: 'In `app/api/assistant/route.ts`, delete the local `onPage` and find the view with `viewAt` from `@/lib/shared-context` (`@meridian/lib/shared-context` in an adopted project): `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.',
579
589
  checks: ['gate'],
@@ -589,7 +599,7 @@ export const RELEASE_MIGRATIONS = [
589
599
  {
590
600
  id: 'today-in-timezone',
591
601
  since: '0.16.3',
592
- 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)),
602
+ applies: (root) => matching(root, (rel, src) => (appPath(rel) === 'api/assistant/route.ts' || rel === 'src/data/metrics.ts') && /\bconst today = clock\(\)\.toISOString\(\)\.slice\(0, 10\)|\btoday: clock\(\)\.toISOString\(\)\.slice\(0, 10\)/.test(src)),
593
603
  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.",
594
604
  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.",
595
605
  checks: ['gate'],
@@ -615,7 +625,7 @@ export const RELEASE_MIGRATIONS = [
615
625
  since: '0.26.0',
616
626
  applies: (root) => matching(root, (rel, src) => importsOf(rel, src).some(({ from }) => from === 'src/components/ui/table' || from === 'src/components/patterns/data-table') && (/@min-\[672px\]:min-w-64/.test(src) || /<TableCell\b[^>]*\bcolSpan\s*=/.test(src))),
617
627
  summary: 'A table is a grid since 0.26.0: its spare width sits in the gaps, a `grow` column\'s floor is 160px at every width, and a cell spans no columns.',
618
- 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`.',
628
+ 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 has a 160px floor at every width. Remove tests that pin the old or new utility-class strings rather than repinning them; check truncation, full-value access and the rendered responsive grid. 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`.',
619
629
  checks: ['gate'],
620
630
  },
621
631
  {
@@ -626,12 +636,20 @@ export const RELEASE_MIGRATIONS = [
626
636
  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\` (\`@meridian/logic/…\` in an adopted project, where \`@/\` is your own). 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.`,
627
637
  checks: GATE_AND_BUILD,
628
638
  },
639
+ {
640
+ id: 'env-reset-before-each',
641
+ since: '0.29.0',
642
+ applies: (root) => matching(root, (rel, src) => /\.test\.[cm]?[jt]sx?$/.test(rel) && setsEnvInBeforeAll(src)),
643
+ summary: '`tests/setup.ts` puts `process.env` back before every test, so a value a test file sets in `beforeAll` is gone when its first test runs.',
644
+ instructions: "Since 0.29.0 the shipped `tests/setup.ts` restores the environment before each test, so a stub in one file can no longer reach the file beside it. A test file of your own that sets `process.env` (or calls `vi.stubEnv`) inside `beforeAll` loses the value before its first test, and fails with no reason given. In each file listed, keep the costly start (a fake backend, a server) in `beforeAll` and keep what it returns in a variable, then set the environment from it in a `beforeEach`: `beforeEach(() => { process.env.API_URL = started.base; })`.",
645
+ checks: ['gate'],
646
+ },
629
647
  {
630
648
  id: 'direct-query-escape',
631
649
  since: '0.30.0',
632
- applies: (root) => matching(root, (rel, src) => /^(?:app|src\/(?:logic|views|lib))\//.test(rel) && SOURCE.test(rel) && /\.query\s*\(/.test(src.replace(/\/\/[^\n]*/g, '')) && !/allow-no-direct-query:/.test(src)),
650
+ applies: (root) => matching(root, (rel, src) => (appPath(rel) !== null || /^src\/(?:logic|views|lib)\//.test(rel)) && SOURCE.test(rel) && /\.query\s*\(/.test(src.replace(/\/\/[^\n]*/g, '')) && !/allow-no-direct-query:/.test(src)),
633
651
  summary: 'A direct `.query(` call is allowed only where the call states its own escape: `// allow-no-direct-query: <why>`, on that line.',
634
- instructions: 'Since 0.30.0 `node scripts/check.ts` reports every `.query(` call in `app/`, `src/logic/`, `src/views/` and `src/lib/`, and keeps the run passing only where the call\'s own line carries the escape the check prints — `// allow-no-direct-query: read() cannot serve this — <why>`. Before this release the rule allowed five sites of the template\'s by line number, so a file of your own that read a collection directly passed as long as the call sat on one of those lines; that list is gone, because a comment added above a call moved it off the line and turned a passing project red for no reason. Read every file named above. Where the read has to be direct — a server action reading the record it is about to change, a streamed export, the sign-in resolving a person before an `AccessScope` exists — state the reason on that line in the form the check prints. Everywhere else, read through `read()`, `readAll()` or `aggregate()` from `@/data/read`, which ask the caller\'s grant.',
652
+ instructions: 'Since 0.30.0 `node scripts/check.ts` reports every `.query(` call in `app/`, `src/app/`, `src/logic/`, `src/views/` and `src/lib/`, and keeps the run passing only where the call\'s own line carries the escape the check prints — `// allow-no-direct-query: read() cannot serve this — <why>`. Before this release the rule allowed five sites of the template\'s by line number, so a file of your own that read a collection directly passed as long as the call sat on one of those lines; that list is gone, because a comment added above a call moved it off the line and turned a passing project red for no reason. Read every file named above. Where the read has to be direct — a server action reading the record it is about to change, a streamed export, the sign-in resolving a person before an `AccessScope` exists — state the reason on that line in the form the check prints. Everywhere else, read through `read()`, `readAll()` or `aggregate()` from `@/data/read`, which ask the caller\'s grant.',
635
653
  checks: ['gate'],
636
654
  },
637
655
  {
@@ -658,6 +676,14 @@ export const RELEASE_MIGRATIONS = [
658
676
  instructions: 'The audit, the presses, the keyboard walk, the vitals and `scripts/shot.ts` now read what a route answered: one that answered an error status or draws the not-found screen (`[data-missing-page]`) was measured as that screen and not as the route, so it fails. In `scripts/verify.config.ts`, which is yours, move every entry of `detailRoutes` that names a record that does not exist (the sample\'s `/requests/req_missing` and `/customers/missing`) into a new `missingRoutes` list; each must be the not-found screen, and one that renders a page fails. A not-found screen of your own carries `data-missing-page` on its root, as `MissingPage` does: inside a streamed shell a missing record has already answered 200.',
659
677
  checks: ['verify'],
660
678
  },
679
+ {
680
+ id: 'computed-series-slot',
681
+ since: '0.32.0',
682
+ applies: (root) => matching(root, (rel, src) => SOURCE.test(rel) && importsOf(rel, src).some(({ from }) => SLOT_CHARTS.includes(from)) && computesSlot(src)),
683
+ summary: "A chart's `color` is a palette slot, `SeriesSlot` (1 to 6), where it took any number: a computed one fails the type check.",
684
+ instructions: "Since 0.32.0 `ColumnPart`, a Composition bar's `Part`, `Sparkline` and a Trend chart's series take `color` as a `SeriesSlot`, `1 | 2 | 3 | 4 | 5 | 6` from `src/components/charts/scale.ts`, because a slot past six named a colour no theme defines and drew nothing. A file of your own that computes the slot (`color: i + 1`) fails `tsc` at `--finalize`. In each file listed, type the value as a `SeriesSlot` where it is computed, or fold the parts past the palette into one \"Other\" part, as the Composition bar does past six. A file that already types it so has nothing to change.",
685
+ checks: ['gate'],
686
+ },
661
687
  {
662
688
  id: 'use-size-callback-ref',
663
689
  since: '0.33.0',
@@ -669,7 +695,7 @@ export const RELEASE_MIGRATIONS = [
669
695
  {
670
696
  id: 'self-hosted-fonts',
671
697
  since: '0.34.0',
672
- applies: (root) => matching(root, (rel, src) => /^app\//.test(rel) && SOURCE.test(rel) && /from\s+['"]next\/font\/google['"]/.test(src) && /--font-face-(?:sans|mono)/.test(src)),
698
+ applies: (root) => matching(root, (rel, src) => appPath(rel) !== null && SOURCE.test(rel) && /from\s+['"]next\/font\/google['"]/.test(src) && /--font-face-(?:sans|mono)/.test(src)),
673
699
  summary: "Meridian's faces ship with the release under `src/styles/fonts/`: a build loads them from there and fetches nothing from Google Fonts.",
674
700
  instructions: "Your `app/layout.tsx` is yours, so an update does not touch it, and yours hands `--font-face-sans` and `--font-face-mono` their faces through `next/font/google`, which `next build` fetches from fonts.googleapis.com: with that host unreachable and no build cache, the build fails (`Build error occurred`). The update has delivered the two faces, `src/styles/fonts/Geist-Variable.woff2` and `src/styles/fonts/GeistMono-Variable.woff2`, with their licence. In the layout, replace the `next/font/google` import with `import localFont from 'next/font/local';` and each face's call with `localFont({ src: '../src/styles/fonts/Geist-Variable.woff2', weight: '100 900', variable: '--font-face-sans', display: 'swap' })` and `localFont({ src: '../src/styles/fonts/GeistMono-Variable.woff2', weight: '100 900', variable: '--font-face-mono', display: 'swap' })`, keeping the class names on `<html>` as they are. A layout that gives the tokens faces of its own from Google Fonts is the same case: ship the files and load them with `next/font/local`.",
675
701
  checks: GATE_AND_BUILD,
@@ -693,11 +719,28 @@ export const RELEASE_MIGRATIONS = [
693
719
  {
694
720
  id: 'sample-with-its-tests',
695
721
  since: '0.36.0',
722
+ recurring: true,
696
723
  applies: sampleBehindItsTests,
697
- summary: "The sample's files changed with the shipped tests that pin them: the update brings the tests, and each file named here is still the copy they no longer pass.",
698
- instructions: "A project `create` made holds the template's sample (its pages, views, data, figures, their helpers and the demo gate in `proxy.ts`) as its own files, which an update never touches, while the shipped tests that pin how they behave are Meridian's and arrive with it. This release changed both together: the API keys actions answer a result, the error screens take `retry`, the request page names its cURL and its payload sizes in characters, the demo gate leaves open only the icon files, a member never seen is said to be, the past-due alert is timed by the latest customer to fall past due, and the period totals moved to the new `src/lib/totals.ts`. Each file named above differs from the release's copy, or is a sample module the release added, and the gate fails on it as it is. Write the release's copies into a scratch folder with `npx zz-meridian@<the version you are updating to> create <scratch folder> --no-install`. Then, for each file named: when your team never changed it (no commit of yours touches it), copy the release's file over it, and copy each module the release added. When your team made it its own, keep yours and make each shipped test that pins it yours too: change the test to your behaviour, which the next update stages for your decision as an edited file, or delete it and name it in `.meridian/keep.json`. A sample file your team deleted is not named.",
724
+ summary: "The release's sample differs from team-owned files, while its shipped tests are managed. Review those differences before finalizing.",
725
+ instructions: "A project `create` made holds the template's sample (its pages, views, data, figures, their helpers and the demo gate in `proxy.ts`) as its own files, which an update never touches, while the shipped tests are Meridian's and arrive with it. The update reports differing sample files on every upgrade, even when a previous release already reported this migration. Each named file differs from the release's copy, or is a sample module introduced after your project's recorded version; the gate may depend on its changes. Write the release's copies into a scratch folder with `npx zz-meridian@<the version you are updating to> create <scratch folder> --no-install`. For a file your team never changed, copy the release's file over it, and copy each module the release added. For a file your team made its own, preserve the intended product behavior while updating its contracts and meaningful behavior and security tests. Keep coverage for functionality the product still exposes; do not delete failures merely to finalize. Retire tests with removed sample features, or when they only pin wording or implementation details, and record intentionally deleted managed tests in `.meridian/keep.json`. A sample file your team deleted after its introduction is not named.",
699
726
  checks: GATE_AND_BUILD,
700
727
  },
728
+ {
729
+ id: 'proposal-approve-required',
730
+ since: '0.37.0',
731
+ applies: (root) => matching(root, (rel, src) => SOURCE.test(rel) && importsOf(rel, src).some(({ from }) => from === 'src/components/patterns/proposal') && tags(src, 'Proposal').some((t) => !/\bstate\s*=/.test(t) && !/\bonApprove\s*=/.test(t) && !/\{\s*\.\.\./.test(t))),
732
+ summary: 'An uncontrolled `Proposal` requires `onApprove`: without it, Approve claimed Applied for a change nothing ran.',
733
+ instructions: 'Since 0.37.0 `src/components/patterns/proposal/index.tsx` types `onApprove` as required when no `state` is passed, so a `<Proposal>` of your own without either fails `tsc` at `--finalize`. In each file listed, pass the function that runs the change as `onApprove`, or pass `state` and own the card\'s states yourself. A card that only shows a change it cannot run is not a proposal: show it in a Card instead.',
734
+ checks: ['gate'],
735
+ },
736
+ {
737
+ id: 'open-command-moved',
738
+ since: '0.37.0',
739
+ applies: (root) => matching(root, (rel, src) => SOURCE.test(rel) && importsOf(rel, src).some(({ from, names }) => from === 'src/components/patterns/command-palette' && names.some((n) => n === 'openCommand' || n === 'usePaletteKey'))),
740
+ summary: '`openCommand` and `usePaletteKey` moved to `src/components/patterns/command-palette/open.ts`, so the palette can be a chunk of its own.',
741
+ instructions: 'Since 0.37.0 the Command palette loads with `next/dynamic` and is in no page\'s first load, so the two things a page needs before it opens, `openCommand` and `usePaletteKey`, live in `src/components/patterns/command-palette/open.ts` and the palette\'s own file no longer exports them. In each file listed, import them from `@/components/patterns/command-palette/open`. Mount the palette the way `src/views/console-chrome.tsx` does, through `dynamic()`, or importing it puts it back in every page\'s first load.',
742
+ checks: ['gate'],
743
+ },
701
744
  ];
702
745
  /**
703
746
  * The template's sample as `create` writes it: the team's own sources among the pages, views, data, logic and library,
@@ -707,10 +750,10 @@ const isSample = (rel) => SOURCE.test(rel) && isTeamOwned(rel) && ((rel.startsWi
707
750
  /** The sample modules 0.36.0 added: a project created before has none, and the release's pages and tests import them. */
708
751
  const SAMPLE_ADDED = ['src/lib/totals.ts'];
709
752
  /**
710
- * 0.36.0: in a project `create` made, each sample file that differs from the release's, and each sample module the
711
- * release added that the project lacks. A sample file the team deleted is its decision and is not named.
753
+ * In a project `create` made, each sample file that differs from the release's, and each sample module introduced
754
+ * after the source release that the project lacks. A file the team deleted after its introduction is not named.
712
755
  */
713
- function sampleBehindItsTests(root) {
756
+ function sampleBehindItsTests(root, source) {
714
757
  if (!/"route"\s*:\s*"create"/.test(text(root, '.meridian/manifest.json') ?? ''))
715
758
  return null;
716
759
  const hits = payloadFiles().filter(isSample).filter((rel) => {
@@ -718,7 +761,7 @@ function sampleBehindItsTests(root) {
718
761
  return !fs.readFileSync(path.join(root, rel)).equals(payloadBytes(rel));
719
762
  }
720
763
  catch {
721
- return SAMPLE_ADDED.includes(rel);
764
+ return SAMPLE_ADDED.includes(rel) && isNewer('0.36.0', source);
722
765
  }
723
766
  });
724
767
  return hits.length > 0 ? hits : null;
@@ -757,12 +800,14 @@ function navItemWithoutFields(root) {
757
800
  * A configuration's own `NavItem` as written, comments blanked: its declaration's line, and when that line does not end
758
801
  * the statement with a `;`, every line after it that is blank, indented, or opens with a closing bracket or an operator
759
802
  * that continues it (`}`, `]`, `)`, `>`, `|`, `&`, `?`, `:`), up to the first that starts at the margin with anything
760
- * else. A declaration inside a comment is not the type unless it is the only one. Undefined when there is none. No
761
- * type is parsed: a shape this cuts short is reported rather than passed, since a migration line with nothing to do costs
762
- * a look, and a missed one is the type error with no line it exists to prevent.
803
+ * else. The type is read whether it is exported or not: one local to the module still types the items of an exported
804
+ * `NavGroup`, so the rail reads its fields all the same (#103). A declaration inside a comment is not the type unless it
805
+ * is the only one. Undefined when there is none. No type is parsed: a shape this cuts short is reported rather than
806
+ * passed, since a migration line with nothing to do costs a look, and a missed one is the type error with no line it
807
+ * exists to prevent.
763
808
  */
764
809
  function navItemType(body) {
765
- const declaration = /^export\s+(?:interface|type)\s+NavItem\b/m;
810
+ const declaration = /^(?:export\s+)?(?:interface|type)\s+NavItem\b/m;
766
811
  // A regular expression is not told from a comment here, so one that opens `/*` can blank the type: then the raw text.
767
812
  const m = declaration.exec(uncommented(body)) ?? declaration.exec(body);
768
813
  if (!m)
@@ -842,7 +887,7 @@ function renamedSampleFiles(root) {
842
887
  const modules = new Set([...old.flatMap((n) => [`src/views/${n}`, `src/views/${n}-context`]), `src/data/${old[1]}`]);
843
888
  const group = `(${old[0]})`;
844
889
  const named = new RegExp(`\\b(?:FEATURES\\.|view_)(?:${old.join('|')})\\b`);
845
- const isOld = (rel) => modules.has(rel.replace(/\.[mc]?[jt]sx?$/, '')) || (/^(?:src\/)?app\//.test(rel) && rel.split('/').includes(group));
890
+ const isOld = (rel) => modules.has(rel.replace(/\.[mc]?[jt]sx?$/, '')) || (appPath(rel) !== null && rel.split('/').includes(group));
846
891
  const sources = teamSources(root);
847
892
  if (!sources.some((f) => isOld(f.rel)))
848
893
  return null;
@@ -854,7 +899,7 @@ function renamedSampleFiles(root) {
854
899
  // of the old, and are as much the sample's. A file that imports one of the old files, such as a test that renders the
855
900
  // page, names it too; a file that only names one, such as the configuration, is not itself the old.
856
901
  const oldFiles = new Set(sources.map((f) => f.rel).filter(isOld));
857
- const pages = sources.filter(({ rel, src }) => /^(?:src\/)?app\//.test(rel) && /^(?:page|view)\.tsx?$/.test(base(rel)) && (isOld(rel) || importsOf(rel, src).some((i) => modules.has(i.from))));
902
+ const pages = sources.filter(({ rel, src }) => appPath(rel) !== null && /^(?:page|view)\.tsx?$/.test(base(rel)) && (isOld(rel) || importsOf(rel, src).some((i) => modules.has(i.from))));
858
903
  for (const { rel } of pages) {
859
904
  const dir = path.posix.dirname(rel);
860
905
  try {
@@ -915,6 +960,31 @@ function navItemObjects(nav) {
915
960
  }
916
961
  return found;
917
962
  }
963
+ /** The chart cards whose `color` is a `SeriesSlot` since 0.32.0. */
964
+ const SLOT_CHARTS = ['src/components/charts/column-chart', 'src/components/charts/column-chart/grouped', 'src/components/charts/composition-bar', 'src/components/charts/sparkline', 'src/components/charts/trend-chart'];
965
+ /**
966
+ * Whether a file computes a chart colour by arithmetic (`color: i + 1`) without typing it `SeriesSlot`: the shape the
967
+ * 0.32.0 narrowing fails. A slot held in a variable typed `number` is not read; a named colour or a literal slot passes.
968
+ */
969
+ function computesSlot(src) {
970
+ return [...src.matchAll(/\bcolor\s*(?::|=\s*\{)([^,}\n]*)/g)].some((m) => /[-+*%]\s*\d|\d\s*[-+*%]/.test(m[1]) && !/\bas\s+SeriesSlot\b/.test(m[1]));
971
+ }
972
+ /**
973
+ * Whether a test file sets the environment inside a `beforeAll`: the hook's own parentheses, read by their nesting, hold
974
+ * an assignment to `process.env` or a `vi.stubEnv` call. Strings are not told from code; a missed shape is a test
975
+ * that fails at `--finalize`, a false one a line to read.
976
+ */
977
+ function setsEnvInBeforeAll(src) {
978
+ for (const m of src.matchAll(/\bbeforeAll\s*\(/g)) {
979
+ let depth = 1;
980
+ let i = m.index + m[0].length;
981
+ for (; i < src.length && depth > 0; i++)
982
+ depth += src[i] === '(' ? 1 : src[i] === ')' ? -1 : 0;
983
+ if (/\bprocess\.env(?:\.\w+|\[[^\]]+\])\s*=(?!=)|\bvi\.stubEnv\s*\(/.test(src.slice(m.index, i)))
984
+ return true;
985
+ }
986
+ return false;
987
+ }
918
988
  /** The project's own nav, with any item still missing the `needs` that 0.13.0 requires of it. */
919
989
  function navItemsWithoutNeeds(root) {
920
990
  const rel = 'src/app.config.ts';
@@ -941,7 +1011,7 @@ function navItemsWithoutNeeds(root) {
941
1011
  */
942
1012
  function ownGroupPagesWithoutAgentReads(root) {
943
1013
  const dirs = [];
944
- for (const app of ['app', 'src/app']) {
1014
+ for (const app of APP_DIRS) {
945
1015
  try {
946
1016
  dirs.push(...fs.readdirSync(path.join(root, app), { withFileTypes: true }).filter((e) => e.isDirectory() && /^\(.+\)$/.test(e.name) && e.name !== '(dashboard)').map((e) => `${app}/${e.name}`));
947
1017
  }
@@ -951,7 +1021,7 @@ function ownGroupPagesWithoutAgentReads(root) {
951
1021
  }
952
1022
  /** The page specifications under the console and embed folders that lack the part the 0.9.0 check requires. */
953
1023
  function pagesWithoutAgentReads(root) {
954
- return specsWithoutAgentReads(root, ['app/(dashboard)', 'app/embed']);
1024
+ return specsWithoutAgentReads(root, APP_DIRS.flatMap((app) => [`${app}/(dashboard)`, `${app}/embed`]));
955
1025
  }
956
1026
  /** The page specifications under `dirs` without `## Agents` and its `### What the agent reads`. */
957
1027
  function specsWithoutAgentReads(root, dirs) {
@@ -963,13 +1033,15 @@ function specsWithoutAgentReads(root, dirs) {
963
1033
  }
964
1034
  /** The registry entries that apply to a project moving from `source` to `target`, as journal migrations. */
965
1035
  export function releaseMigrations(root, source, target) {
1036
+ if (!isNewer(target, source))
1037
+ return [];
966
1038
  const out = [];
967
1039
  sources = new Map();
968
1040
  try {
969
1041
  for (const m of RELEASE_MIGRATIONS) {
970
- if (!isNewer(m.since, source) || isNewer(m.since, target))
1042
+ if (isNewer(m.since, target) || (!m.recurring && !isNewer(m.since, source)))
971
1043
  continue;
972
- const paths = m.applies(root);
1044
+ const paths = m.applies(root, source);
973
1045
  if (paths)
974
1046
  out.push({ id: m.id, summary: m.summary, paths, instructions: m.instructions, checks: m.checks });
975
1047
  }
package/dist/reconcile.js CHANGED
@@ -20,6 +20,11 @@ function range(spec) {
20
20
  }
21
21
  const minimum = (spec) => range(spec)?.v ?? null;
22
22
  const compare = (a, b) => a[0] - b[0] || a[1] - b[1] || a[2] - b[2];
23
+ /** Compare supported dependency floors; null means the specification needs a person's judgment. */
24
+ export function compareMinimum(have, want) {
25
+ const a = minimum(have), b = minimum(want);
26
+ return a && b ? compare(a, b) : null;
27
+ }
23
28
  /** Whether `version` satisfies a specification `range` reads. A specification it cannot read is not judged. */
24
29
  export function satisfies(spec, version) {
25
30
  const want = range(spec);
package/dist/session.js CHANGED
@@ -598,9 +598,9 @@ function patchReport(l, outcome, coverage = null) {
598
598
  .replace(/^- Coverage: .*\n/m, '')
599
599
  .replace(/^## Failure\n\n[\s\S]*?\n\n(?=## )/m, '');
600
600
  if (coverage)
601
- text = text.replace(/^(- Outcome: .*)$/m, `$1\n- Coverage: ${coverage.replace(/^coverage: /, '')}`);
601
+ text = text.replace(/^(- Outcome: .*)$/m, (line) => `${line}\n- Coverage: ${coverage.replace(/^coverage: /, '')}`);
602
602
  if (j.failure)
603
- text = text.replace(/^## What needs you$/m, `## Failure\n\n${j.failure}\n\n## What needs you`);
603
+ text = text.replace(/^## What needs you$/m, () => `## Failure\n\n${j.failure}\n\n## What needs you`);
604
604
  writeAtomic(file, text);
605
605
  }
606
606
  /** Drops the temporary copies and moves the session to `.meridian/history/<version>/<id>/`. */