@ultimat3/cli 29.4.0 → 30.1.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 (448) hide show
  1. package/CLAUDE.md +64 -40
  2. package/README.md +61 -11
  3. package/package.json +42 -28
  4. package/src/affected-tests.ts +97 -0
  5. package/src/affected-unowned.ts +43 -0
  6. package/src/affected.ts +25 -6
  7. package/src/app-boundaries.ts +29 -1
  8. package/src/app-evals.ts +12 -4
  9. package/src/app-load-unresolved.ts +62 -0
  10. package/src/app-load.ts +30 -37
  11. package/src/app-mcp.ts +14 -2
  12. package/src/app-module-graph.ts +232 -0
  13. package/src/app-reload-graph.ts +2 -2
  14. package/src/app-root.ts +37 -1
  15. package/src/bin.ts +1 -0
  16. package/src/boundary-findings.ts +6 -0
  17. package/src/budgets-freshness.ts +32 -0
  18. package/src/budgets.ts +20 -1
  19. package/src/build-capture-env.ts +9 -0
  20. package/src/cdp-shot-a11y.ts +2 -39
  21. package/src/ci-tested.ts +243 -0
  22. package/src/client-emit-customer-runtime.ts +139 -0
  23. package/src/client-emit-customer.ts +108 -0
  24. package/src/client-emit-drift.ts +72 -0
  25. package/src/client-emit-header.ts +67 -0
  26. package/src/client-emit-schemas.ts +175 -0
  27. package/src/client-emit-types.ts +121 -0
  28. package/src/client-emit-vendor.ts +146 -0
  29. package/src/client-errors-routes.ts +229 -0
  30. package/src/client-errors-script.ts +74 -0
  31. package/src/client-vitals-route.ts +197 -0
  32. package/src/client-vitals-script.ts +56 -0
  33. package/src/cmd-affected.ts +23 -0
  34. package/src/cmd-build-spec.ts +7 -1
  35. package/src/cmd-build.ts +35 -9
  36. package/src/cmd-ci-spec.ts +42 -6
  37. package/src/cmd-ci.ts +99 -2
  38. package/src/cmd-client-customer.ts +134 -0
  39. package/src/cmd-client-source.ts +81 -0
  40. package/src/cmd-client-spec.ts +48 -0
  41. package/src/cmd-client-write.ts +41 -0
  42. package/src/cmd-client.ts +111 -0
  43. package/src/cmd-db-recount.ts +120 -0
  44. package/src/cmd-db-spec.ts +36 -6
  45. package/src/cmd-db.ts +11 -3
  46. package/src/cmd-deploy-compose.ts +61 -3
  47. package/src/cmd-deploy-drain.ts +10 -2
  48. package/src/cmd-deploy-helm.ts +11 -0
  49. package/src/cmd-deploy-preflight.ts +118 -0
  50. package/src/cmd-deploy-spec.ts +58 -6
  51. package/src/cmd-deploy-verify.ts +380 -0
  52. package/src/cmd-deploy.ts +42 -9
  53. package/src/cmd-docs.ts +34 -4
  54. package/src/cmd-doctor.ts +66 -9
  55. package/src/cmd-drill-spec.ts +25 -0
  56. package/src/cmd-drill.ts +176 -0
  57. package/src/cmd-env-init.ts +102 -0
  58. package/src/cmd-env-spec.ts +10 -3
  59. package/src/cmd-env.ts +15 -10
  60. package/src/cmd-errors-recent.ts +60 -0
  61. package/src/cmd-errors-spec.ts +9 -1
  62. package/src/cmd-errors.ts +4 -0
  63. package/src/cmd-exercise-spec.ts +19 -0
  64. package/src/cmd-exercise.ts +129 -0
  65. package/src/cmd-generate-spec.ts +9 -1
  66. package/src/cmd-generate.ts +32 -27
  67. package/src/cmd-graph-load.ts +97 -0
  68. package/src/cmd-graph-spec.ts +16 -0
  69. package/src/cmd-graph.ts +73 -0
  70. package/src/cmd-i18n.ts +4 -1
  71. package/src/cmd-impact-spec.ts +13 -0
  72. package/src/cmd-impact.ts +64 -0
  73. package/src/cmd-jobs-resolve.ts +76 -0
  74. package/src/cmd-jobs-spec.ts +21 -2
  75. package/src/cmd-jobs.ts +10 -6
  76. package/src/cmd-mail-spec.ts +28 -0
  77. package/src/cmd-mail.ts +70 -0
  78. package/src/cmd-mcp-lock.ts +69 -0
  79. package/src/cmd-mcp-spec.ts +23 -4
  80. package/src/cmd-mcp.ts +17 -3
  81. package/src/cmd-new-spec.ts +8 -1
  82. package/src/cmd-new.ts +61 -9
  83. package/src/cmd-planned.ts +0 -6
  84. package/src/cmd-repro-spec.ts +31 -0
  85. package/src/cmd-repro.ts +174 -0
  86. package/src/cmd-stop-spec.ts +21 -0
  87. package/src/cmd-stop.ts +166 -0
  88. package/src/cmd-test-spec.ts +17 -4
  89. package/src/cmd-test.ts +70 -14
  90. package/src/cmd-upgrade-spec.ts +26 -0
  91. package/src/cmd-upgrade.ts +274 -0
  92. package/src/cmd-verify-spec.ts +3 -3
  93. package/src/db-backfill.ts +6 -0
  94. package/src/db-diagnostics.ts +106 -0
  95. package/src/db-generate.ts +9 -0
  96. package/src/db-reset-template.ts +1 -1
  97. package/src/db-rolling.ts +45 -0
  98. package/src/db-seed-generate.ts +188 -0
  99. package/src/deploy-verify-findings.ts +98 -0
  100. package/src/deploy-verify-flags.ts +109 -0
  101. package/src/deploy-verify-grade.ts +185 -0
  102. package/src/deploy-verify-queues.ts +53 -0
  103. package/src/dev-boot.ts +35 -15
  104. package/src/dev-child-watch.ts +16 -4
  105. package/src/dev-dashboard-guard.ts +8 -1
  106. package/src/dev-dashboard.ts +7 -0
  107. package/src/dev-lock-identity.ts +61 -0
  108. package/src/dev-lock-port-holder.ts +80 -0
  109. package/src/dev-lock-steal.ts +124 -0
  110. package/src/dev-lock.ts +92 -120
  111. package/src/dev-mail-templates.ts +51 -0
  112. package/src/dev-rebuild.ts +26 -2
  113. package/src/dev-route-table.ts +17 -0
  114. package/src/dev-supervisor.ts +5 -2
  115. package/src/dev-watch-tree.ts +11 -10
  116. package/src/dev-watch.ts +1 -1
  117. package/src/dispatch.ts +29 -6
  118. package/src/docs-section.ts +69 -0
  119. package/src/docs-sources.ts +100 -0
  120. package/src/doctor-auth.ts +11 -2
  121. package/src/doctor-chart-alerts.ts +55 -0
  122. package/src/doctor-pooler.ts +107 -0
  123. package/src/doctor-rolling.ts +35 -0
  124. package/src/doctor-row-security.ts +40 -0
  125. package/src/document-options.ts +13 -0
  126. package/src/drill-sync-lane.ts +25 -0
  127. package/src/drill-targets.ts +39 -0
  128. package/src/duplicate-packages.ts +1 -1
  129. package/src/env-production.ts +93 -0
  130. package/src/error-codes-shot.ts +59 -0
  131. package/src/error-codes-tooling.ts +29 -0
  132. package/src/error-codes.ts +100 -54
  133. package/src/error-contract.ts +2 -2
  134. package/src/errors.ts +22 -3
  135. package/src/exercise-database.ts +99 -0
  136. package/src/exercise-findings.ts +159 -0
  137. package/src/exercise-overrides.ts +42 -0
  138. package/src/exercise-registration.ts +58 -0
  139. package/src/exercise-static.ts +88 -0
  140. package/src/exercise-subject.ts +69 -0
  141. package/src/failure-capture-boot.ts +30 -0
  142. package/src/framework-env.ts +7 -1
  143. package/src/framework-schema.ts +72 -5
  144. package/src/generate-catalog-locales.ts +19 -0
  145. package/src/generate-chart-alerts.ts +156 -0
  146. package/src/generate-feature-required.ts +65 -0
  147. package/src/generate-feature.ts +83 -6
  148. package/src/generate-files.ts +24 -0
  149. package/src/generate-format-written.ts +32 -0
  150. package/src/generate-kinds.ts +6 -0
  151. package/src/generate-sign-in-config.ts +100 -0
  152. package/src/generate-sign-in-manifest.ts +28 -0
  153. package/src/generate-sign-in-mode.ts +101 -0
  154. package/src/generate-sign-in.ts +250 -0
  155. package/src/generate-write.ts +44 -7
  156. package/src/hand-rolled-socket.ts +72 -0
  157. package/src/i18n-app-key-shapes.ts +29 -0
  158. package/src/i18n-audit.ts +10 -3
  159. package/src/i18n-registration.ts +18 -3
  160. package/src/index.ts +2 -0
  161. package/src/island-bundle.ts +4 -3
  162. package/src/island-disposer.ts +116 -0
  163. package/src/island-realtime-state.ts +34 -6
  164. package/src/island-realtime.ts +6 -2
  165. package/src/island-routes.ts +3 -2
  166. package/src/isr-query-advice.ts +6 -11
  167. package/src/isr-query-guard.ts +4 -3
  168. package/src/job-budget.ts +95 -0
  169. package/src/jobs-json.ts +11 -0
  170. package/src/lint-findings.ts +104 -0
  171. package/src/local-cli.ts +34 -4
  172. package/src/loopback-host.ts +2 -2
  173. package/src/mail-preview-findings.ts +24 -0
  174. package/src/mcp-errors.ts +82 -2
  175. package/src/mcp-host.ts +11 -1
  176. package/src/mcp-lock-findings.ts +57 -0
  177. package/src/measure-paths.ts +12 -4
  178. package/src/measure-scope.ts +27 -13
  179. package/src/messages-client.ts +23 -0
  180. package/src/messages-db.ts +56 -0
  181. package/src/messages-gate.ts +107 -0
  182. package/src/messages-repro.ts +11 -0
  183. package/src/messages-shot.ts +51 -0
  184. package/src/messages-upgrade.ts +45 -0
  185. package/src/messages.ts +53 -178
  186. package/src/metrics-endpoint.ts +23 -5
  187. package/src/new-shapes-generate.ts +71 -0
  188. package/src/new-shapes.ts +63 -0
  189. package/src/openapi-compile-ir.ts +68 -0
  190. package/src/openapi-compile-schema.ts +284 -0
  191. package/src/openapi-compile.ts +273 -0
  192. package/src/openapi-refuse.ts +45 -0
  193. package/src/otlp-export.ts +2 -1
  194. package/src/output-findings.ts +123 -0
  195. package/src/output.ts +32 -8
  196. package/src/package-boundaries.ts +110 -0
  197. package/src/page-pins.ts +23 -0
  198. package/src/plain-routes.ts +13 -0
  199. package/src/prerender-entries.ts +59 -0
  200. package/src/prerender-heaviest.ts +27 -0
  201. package/src/prerender-preload.ts +6 -0
  202. package/src/prerender-report.ts +12 -0
  203. package/src/prerender-seo-reads.ts +38 -0
  204. package/src/prerender-served.ts +22 -7
  205. package/src/prerender-vitals.ts +42 -0
  206. package/src/prerender.ts +61 -32
  207. package/src/pwa-artifacts.ts +4 -3
  208. package/src/pwa-manifest-locales.ts +1 -1
  209. package/src/registry.ts +18 -0
  210. package/src/repro-bundle.ts +65 -0
  211. package/src/repro-errors.ts +47 -0
  212. package/src/repro-job.ts +72 -0
  213. package/src/repro-literal.ts +41 -0
  214. package/src/repro-request.ts +48 -0
  215. package/src/repro-write.ts +100 -0
  216. package/src/retain-sweep.ts +27 -0
  217. package/src/retained-assets-boot.ts +113 -0
  218. package/src/retained-assets-layout.ts +110 -0
  219. package/src/retained-assets-read.ts +142 -0
  220. package/src/retained-assets-sweep.ts +70 -0
  221. package/src/retained-assets.ts +171 -0
  222. package/src/revalidated-response.ts +8 -5
  223. package/src/role-replicator.ts +7 -1
  224. package/src/role-start-devices.ts +119 -0
  225. package/src/role-start-types.ts +6 -0
  226. package/src/role-start.ts +22 -5
  227. package/src/role-sync.ts +7 -1
  228. package/src/route-islands.ts +42 -0
  229. package/src/route-table.ts +2 -1
  230. package/src/row-security-boot.ts +58 -0
  231. package/src/row-security-wanted.ts +32 -0
  232. package/src/runtime-admin.ts +0 -1
  233. package/src/runtime-assets-site-cache.ts +55 -0
  234. package/src/runtime-assets-site.ts +136 -0
  235. package/src/runtime-assets.ts +99 -55
  236. package/src/runtime-bindings.ts +19 -3
  237. package/src/runtime-cache.ts +23 -14
  238. package/src/runtime-isr-outcome.ts +114 -60
  239. package/src/runtime-purge.ts +14 -2
  240. package/src/runtime-queue.ts +31 -4
  241. package/src/runtime-render-document.ts +204 -0
  242. package/src/runtime-render.ts +48 -198
  243. package/src/runtime-replica-position.ts +62 -0
  244. package/src/runtime-replica.ts +18 -3
  245. package/src/runtime-s3-path-style.ts +17 -0
  246. package/src/runtime-services.ts +19 -6
  247. package/src/runtime-statement-bound.ts +22 -0
  248. package/src/runtime-storage-upload.ts +9 -0
  249. package/src/runtime-storage.ts +41 -81
  250. package/src/schema-diff-views.ts +54 -0
  251. package/src/schema-diff.ts +21 -3
  252. package/src/schema-drift.ts +28 -9
  253. package/src/schema-dump-drift.ts +91 -7
  254. package/src/script-csp.ts +1 -1
  255. package/src/seo-cards.ts +26 -0
  256. package/src/seo-routes.ts +22 -8
  257. package/src/serve-boot.ts +54 -21
  258. package/src/serve-drain.ts +1 -0
  259. package/src/serve-env.ts +26 -8
  260. package/src/serve-retire.ts +12 -6
  261. package/src/serve-web.ts +42 -4
  262. package/src/serve.ts +9 -7
  263. package/src/site-asset-routes.ts +7 -1
  264. package/src/site-config.ts +4 -0
  265. package/src/site-llms-pages.ts +106 -0
  266. package/src/site-llms.ts +64 -0
  267. package/src/site-seo-write.ts +4 -2
  268. package/src/site-seo.ts +55 -6
  269. package/src/source-mtime.ts +50 -0
  270. package/src/static-capture-errors.ts +41 -0
  271. package/src/static-capture.ts +56 -0
  272. package/src/static-cards-serve.ts +39 -0
  273. package/src/static-cards.ts +93 -0
  274. package/src/static-document.ts +14 -13
  275. package/src/static-images.ts +115 -0
  276. package/src/stream-hole-scope.ts +55 -0
  277. package/src/style-bundle.ts +1 -1
  278. package/src/style-routes.ts +3 -2
  279. package/src/sw-precache-plan.ts +2 -2
  280. package/src/sw-routes.ts +2 -2
  281. package/src/templates/action-tests.ts +306 -0
  282. package/src/templates/action.ts +81 -269
  283. package/src/templates/backfill.ts +1 -1
  284. package/src/templates/entity.ts +21 -5
  285. package/src/templates/github/ci.yml.ts +8 -9
  286. package/src/templates/guard-bare-error.ts +2 -2
  287. package/src/templates/guard-error-declared.ts +263 -0
  288. package/src/templates/guard-native-confirm.ts +132 -0
  289. package/src/templates/guard-raw-fetch.ts +151 -0
  290. package/src/templates/guard-raw-interval.ts +121 -0
  291. package/src/templates/guard-repo-raw-sql.ts +71 -2
  292. package/src/templates/job.ts +1 -1
  293. package/src/templates/naming.ts +5 -0
  294. package/src/templates/pinned/docker-compose.yml +6 -0
  295. package/src/templates/pinned-images.ts +8 -0
  296. package/src/templates/policy.ts +85 -2
  297. package/src/templates/query.ts +70 -13
  298. package/src/templates/resource-create.ts +3 -1
  299. package/src/templates/resource-form-island.ts +15 -4
  300. package/src/templates/resource-page-db.ts +83 -0
  301. package/src/templates/resource-page.ts +161 -52
  302. package/src/templates/route.ts +4 -6
  303. package/src/templates/scaffold-app.ts +3 -0
  304. package/src/templates/scaffold-auth.ts +2 -1
  305. package/src/templates/scaffold-bin.ts +5 -52
  306. package/src/templates/scaffold-claude-agents.ts +19 -19
  307. package/src/templates/scaffold-claude-commands.ts +28 -31
  308. package/src/templates/scaffold-claude.ts +26 -23
  309. package/src/templates/scaffold-container-compose.ts +6 -1
  310. package/src/templates/scaffold-container.ts +24 -4
  311. package/src/templates/scaffold-dashboard-bare.ts +3 -3
  312. package/src/templates/scaffold-dashboard-example-tests.ts +49 -9
  313. package/src/templates/scaffold-dashboard-example-view.ts +156 -0
  314. package/src/templates/scaffold-dashboard-example.ts +43 -131
  315. package/src/templates/scaffold-dashboard-shared.ts +3 -7
  316. package/src/templates/scaffold-db-client.ts +20 -6
  317. package/src/templates/scaffold-db-package.ts +5 -14
  318. package/src/templates/scaffold-docs.ts +73 -57
  319. package/src/templates/scaffold-domain-package.ts +18 -14
  320. package/src/templates/scaffold-entries.ts +7 -2
  321. package/src/templates/scaffold-exercise.ts +37 -0
  322. package/src/templates/scaffold-guards.ts +13 -1
  323. package/src/templates/scaffold-helm-alerts.ts +234 -0
  324. package/src/templates/scaffold-helm-templates.ts +7 -5
  325. package/src/templates/scaffold-helm.ts +46 -12
  326. package/src/templates/scaffold-i18n.ts +6 -2
  327. package/src/templates/scaffold-mcp-endpoint.ts +182 -0
  328. package/src/templates/scaffold-mcp-json.ts +31 -0
  329. package/src/templates/scaffold-mcp-package.ts +59 -56
  330. package/src/templates/scaffold-repo.ts +7 -2
  331. package/src/templates/scaffold-shell.ts +45 -79
  332. package/src/templates/scaffold-test-services.ts +13 -5
  333. package/src/templates/shapes/api-keys.ts +170 -0
  334. package/src/templates/shapes/api.ts +101 -0
  335. package/src/templates/shapes/blog-site.ts +340 -0
  336. package/src/templates/shapes/blog-slice.ts +265 -0
  337. package/src/templates/shapes/blog.ts +114 -0
  338. package/src/templates/shapes/dashboard-detail.ts +242 -0
  339. package/src/templates/shapes/dashboard.ts +53 -0
  340. package/src/templates/shapes/index.ts +17 -0
  341. package/src/templates/shapes/overlay.ts +99 -0
  342. package/src/templates/shapes/shared-edits.ts +46 -0
  343. package/src/templates/sign-in-action-tests.ts +237 -0
  344. package/src/templates/sign-in-authenticator.ts +112 -0
  345. package/src/templates/sign-in-oidc.ts +189 -0
  346. package/src/templates/sign-in-pages.ts +148 -0
  347. package/src/templates/sign-in-password.ts +216 -0
  348. package/src/templates/sign-in-shared.ts +268 -0
  349. package/src/templates/slice-foundation.ts +70 -15
  350. package/src/test-batches.ts +1 -1
  351. package/src/test-database.ts +1 -1
  352. package/src/test-shards.ts +7 -1
  353. package/src/test-workers.ts +18 -12
  354. package/src/tool-gate-names.ts +34 -0
  355. package/src/upgrade-agents-link.ts +49 -0
  356. package/src/upgrade-bun-floor.ts +225 -0
  357. package/src/upgrade-gate.ts +228 -0
  358. package/src/upgrade-pins.ts +63 -0
  359. package/src/upgrade-plan.ts +214 -0
  360. package/src/upgrade-registry.ts +53 -0
  361. package/src/upgrade-report.ts +139 -0
  362. package/src/upgrade-root.ts +74 -0
  363. package/src/verify-budgets-build.ts +118 -0
  364. package/src/verify-checks.ts +67 -24
  365. package/src/verify-coverage-run.ts +1 -1
  366. package/src/verify-e2e-lane.ts +41 -0
  367. package/src/verify-exercise.ts +64 -0
  368. package/src/verify-floor.ts +41 -0
  369. package/src/verify-merge.ts +19 -4
  370. package/src/verify-role-load.ts +2 -0
  371. package/src/verify-run.ts +28 -8
  372. package/src/verify-shard.ts +12 -2
  373. package/src/verify-skip-ceiling.ts +87 -0
  374. package/src/verify-step.ts +5 -0
  375. package/src/verify-tests.ts +25 -5
  376. package/src/view-drift.ts +55 -0
  377. package/src/worker-bundle.ts +3 -2
  378. package/src/workspace-graph.ts +38 -19
  379. package/src/write-generated.ts +43 -0
  380. package/src/zone-boot.ts +20 -0
  381. package/wiki/Actions.md +342 -0
  382. package/wiki/Admin-Dashboard.md +432 -0
  383. package/wiki/Agents.md +958 -0
  384. package/wiki/Auth.md +195 -0
  385. package/wiki/Bare-VM.md +111 -0
  386. package/wiki/Batching-And-Preloading.md +136 -0
  387. package/wiki/Building-Your-Own-Base.md +203 -0
  388. package/wiki/CI-Parallel-Gate.md +220 -0
  389. package/wiki/CLI-Reference.md +1894 -0
  390. package/wiki/Caching-And-Invalidation.md +255 -0
  391. package/wiki/Client-Data.md +194 -0
  392. package/wiki/Client-Navigation.md +513 -0
  393. package/wiki/Compared.md +93 -0
  394. package/wiki/Configuration.md +868 -0
  395. package/wiki/Contributing.md +12 -0
  396. package/wiki/Deploy-Recipes.md +122 -0
  397. package/wiki/Deployment.md +508 -0
  398. package/wiki/Entities-And-Migrations.md +1116 -0
  399. package/wiki/FAQ.md +175 -0
  400. package/wiki/Feature-Flags.md +65 -0
  401. package/wiki/Getting-Started.md +221 -0
  402. package/wiki/Home.md +160 -0
  403. package/wiki/I18n.md +239 -0
  404. package/wiki/Installation.md +206 -0
  405. package/wiki/Interface-Rules.md +339 -0
  406. package/wiki/Jobs-And-Workflows.md +783 -0
  407. package/wiki/Known-Gaps.md +185 -0
  408. package/wiki/MCP-And-AI.md +753 -0
  409. package/wiki/Mail.md +138 -0
  410. package/wiki/Migrating-An-Existing-App.md +594 -0
  411. package/wiki/Migrating-From-Django.md +54 -0
  412. package/wiki/Migrating-From-Laravel.md +53 -0
  413. package/wiki/Migrating-From-Node-Drizzle.md +53 -0
  414. package/wiki/Migrating-From-Rails.md +53 -0
  415. package/wiki/Migrations-And-Backfills.md +552 -0
  416. package/wiki/Money.md +338 -0
  417. package/wiki/N-Plus-One-Detection.md +197 -0
  418. package/wiki/Notify.md +208 -0
  419. package/wiki/Observability.md +245 -0
  420. package/wiki/Outbound-HTTP.md +109 -0
  421. package/wiki/PWA-And-Offline.md +343 -0
  422. package/wiki/Policies-And-Authz.md +251 -0
  423. package/wiki/Project-Layout.md +150 -0
  424. package/wiki/Queries-And-Live-Queries.md +429 -0
  425. package/wiki/Realtime.md +734 -0
  426. package/wiki/Resource-Management.md +121 -0
  427. package/wiki/Routes-And-Render-Modes.md +521 -0
  428. package/wiki/SEO.md +288 -0
  429. package/wiki/Scheduled-Tasks.md +175 -0
  430. package/wiki/Scraping.md +493 -0
  431. package/wiki/Shapes.md +63 -0
  432. package/wiki/Stability.md +97 -0
  433. package/wiki/Static-Assets.md +166 -0
  434. package/wiki/Storage-And-Uploads.md +404 -0
  435. package/wiki/Testing.md +1314 -0
  436. package/wiki/The-Eight-Primitives.md +247 -0
  437. package/wiki/Theming.md +362 -0
  438. package/wiki/Timezones-And-Dates.md +194 -0
  439. package/wiki/Troubleshooting.md +163 -0
  440. package/wiki/Tutorial-01-First-App.md +289 -0
  441. package/wiki/Tutorial-02-First-Feature.md +339 -0
  442. package/wiki/Tutorial-03-Auth-And-Admin.md +379 -0
  443. package/wiki/Tutorial-04-Jobs-And-Realtime.md +263 -0
  444. package/wiki/Tutorial-05-Deploy-Free.md +231 -0
  445. package/wiki/Tutorial-06-Growing-Up.md +158 -0
  446. package/wiki/UI-Components.md +156 -0
  447. package/wiki/Upgrading.md +3667 -0
  448. package/src/stored-object-headers.ts +0 -43
@@ -6,7 +6,7 @@
6
6
  import type { GeneratedFile, NameSet } from './naming';
7
7
 
8
8
  const feature = (app: NameSet): string => `---
9
- description: Build or fix one thing in ${app.kebab} end to end — name the primitive, generate it, wire it inside the boundaries, gate it with \`bun run check\`.
9
+ description: Build or fix one thing in ${app.kebab} end to end — name the primitive, generate it, wire it inside the boundaries, gate it with \`bun run verify\`.
10
10
  argument-hint: <what you want built or fixed, plain language>
11
11
  allowed-tools: Read, Write, Edit, Glob, Grep, Bash, Agent, Skill
12
12
  ---
@@ -16,7 +16,7 @@ allowed-tools: Read, Write, Edit, Glob, Grep, Bash, Agent, Skill
16
16
  You are a senior engineer on **${app.kebab}**, an Ultimate app. Read \`AGENTS.md\` before designing
17
17
  anything — it is the short form of every rule below, and it wins where the two disagree.
18
18
 
19
- **Done means \`bun run check\` green.** A passing unit test is not done. A working \`x dev\` is not done.
19
+ **Done means \`bun run verify\` green.** A passing unit test is not done. A working \`bunx x dev\` is not done.
20
20
  Report what you actually ran, never what you assume passed.
21
21
 
22
22
  ## Request
@@ -49,15 +49,15 @@ obvious, hand the request to the \`shape\` subagent — that pass is the whole r
49
49
  ## 2. Generate it — do not hand-write it
50
50
 
51
51
  \`\`\`sh
52
- x g <kind> <name> --feature <slice> # x g --help is the only list of kinds
52
+ bunx x g <kind> <name> --feature <slice> # bunx x g --help is the only list of kinds
53
53
  \`\`\`
54
54
 
55
55
  The generator writes the source, its test and its i18n keys as one unit and registers it. A
56
- hand-written primitive compiles and then goes missing from \`x routes\`, \`x actions\`, \`x queries\`,
57
- \`x jobs\`, \`x tasks\`, \`x policy list\` and \`x.manifest.json\` — every surface that is supposed to
56
+ hand-written primitive compiles and then goes missing from \`bunx x routes\`, \`bunx x actions\`, \`bunx x queries\`,
57
+ \`bunx x jobs\`, \`bunx x tasks\`, \`bunx x policy list\` and \`x.manifest.json\` — every surface that is supposed to
58
58
  project it. Edit what the generator wrote; never reproduce it.
59
59
 
60
- Schema changes are generated too: edit \`entity.ts\`, then \`x db gen "what changed"\`. Never hand-write
60
+ Schema changes are generated too: edit \`entity.ts\`, then \`bunx x db gen "what changed"\`. Never hand-write
61
61
  SQL into \`packages/db/migrations/\`.
62
62
 
63
63
  ## 3. Wire it inside the boundaries
@@ -72,23 +72,21 @@ SQL into \`packages/db/migrations/\`.
72
72
  | routes | call actions and queries, never a repo | policy skipped |
73
73
 
74
74
  Route files: \`page.tsx\` under \`site/\`/\`app/\`, and none under \`api/\`. **The directory is the URL** —
75
- the filename never is. One interactive control on a 0kb page is an island: \`x g island <name> --at <dir>\`.
75
+ the filename never is. One interactive control on a 0kb page is an island: \`bunx x g island <name> --at <dir>\`.
76
76
 
77
77
  ## 4. Gate it
78
78
 
79
79
  \`\`\`sh
80
- bun run check # the gate. green = shippable
81
- bun run check --json # the same, machine-readable
80
+ bun run verify # the gate. green = shippable
81
+ bun run verify --json # the same, machine-readable
82
82
  \`\`\`
83
83
 
84
- \`bun run check\` is \`x build --target static\` and THEN \`x verify\`, and the order is the whole point:
85
- the \`budgets\` step measures \`.x/build-stats.json\`, which only a build writes, so \`x verify\` on
86
- its own reports X_BUDGET_UNMEASURED on a tree that is fine. \`--json\` is forwarded to both halves;
87
- a machine consumer takes the last line. CI runs this same script — \`.github/workflows/ci.yml\` is
88
- \`bun run setup\` and then \`bun run check\`, nothing else.
84
+ \`bun run verify\` is \`bunx x verify\`, the whole gate: its \`budgets\` step builds what it weighs when
85
+ \`.x/build-stats.json\` is missing or older than the source. CI runs this same script —
86
+ \`.github/workflows/ci.yml\` is \`bun run setup\` and then \`bun run verify\`, nothing else.
89
87
 
90
88
  Red is instructions, not a verdict: **every finding carries an executable \`fix:\` — run it verbatim
91
- before improvising**, and \`x errors explain <CODE>\` expands any code it names. Never narrow the gate
89
+ before improvising**, and \`bunx x errors explain <CODE>\` expands any code it names. Never narrow the gate
92
90
  to make it pass: there is no \`--only\` and no \`--skip\`, on purpose, and disabling a lint rule or
93
91
  loosening a compiler flag is the same move wearing a different hat.
94
92
 
@@ -98,15 +96,16 @@ While you iterate, narrow the *feedback*, not the gate:
98
96
  |---|---|
99
97
  | one test file | \`bun test <path>/<file>.test.ts\` |
100
98
  | one test by name | \`bun test -t '<name>'\` |
101
- | a whole type | \`x test unit\` · \`x test contract\` · \`x test e2e\` |
99
+ | a whole type | \`bunx x test unit\` · \`bunx x test contract\` · \`bunx x test e2e\` |
102
100
  | lint the files you touched | \`bunx biome check --write <paths>\` |
103
101
  | types, once, when otherwise done | \`bun run typecheck\` |
104
- | a broken environment | \`x doctor\` |
102
+ | a broken environment | \`bunx x doctor\` |
105
103
 
106
104
  ## 5. Hard rules
107
105
 
108
- Never a bare \`throw new Error\` — subclass \`UltimateError\` with a stable code, a cause and a
109
- runnable \`fix:\`. No \`any\`; use \`unknown\` and parse. Named exports only. \`import type\` for types.
106
+ Never a bare \`throw new Error\` — declare each code once with \`defineError\` from \`@ultimat3/core\`:
107
+ a stable code, a title, a status, a cause and a runnable \`fix:\`.
108
+ No \`any\`; use \`unknown\` and parse. Named exports only. \`import type\` for types.
110
109
  Tests next to the source as \`<file>.test.ts\`, failure case first — a test that cannot fail is not a
111
110
  test. One file, one job. Every user-facing string through \`t()\`. Semantic tokens, never a raw
112
111
  colour. Every date formatted with an explicit IANA time zone. Money is integer minor units plus an
@@ -116,9 +115,9 @@ ISO code, never a float. Bun only.
116
115
 
117
116
  \`\`\`
118
117
  Primitive: <which of the eight> Slice: <dir>
119
- Generated: <the x g invocations you ran>
118
+ Generated: <every \`bunx x g\` command you ran>
120
119
  Changed: <files>
121
- Gate: bun run check ✓ | ✗ <failing steps>
120
+ Gate: bun run verify ✓ | ✗ <failing steps>
122
121
  Deferred: <what you did not do, and why> [never omit this line]
123
122
  \`\`\`
124
123
  `;
@@ -166,13 +165,13 @@ slice it lives in. If it fits none, the design is wrong — say so here instead
166
165
  - \`path:line\` — what changes, and why.
167
166
 
168
167
  ## Steps
169
- 1. Ordered, concrete. Name the \`x g\` invocation where one applies. Point at code, do not paste it.
168
+ 1. Ordered, concrete. Name the \`bunx x g\` invocation where one applies. Point at code, do not paste it.
170
169
 
171
170
  ## Tests
172
171
  - What to add, next to the source as \`<file>.test.ts\`. Command to run it.
173
172
 
174
173
  ## Done when
175
- - Acceptance criteria, ending in \`bun run check\` green.
174
+ - Acceptance criteria, ending in \`bun run verify\` green.
176
175
 
177
176
  ## Risks
178
177
  - Anything the executor must decide, and every claim in the ask the code disproves.
@@ -185,7 +184,7 @@ slice it lives in. If it fits none, the design is wrong — say so here instead
185
184
  - No checkboxes. The plan is a map, not a tracker.
186
185
  - The plan must obey the app's own rules — one way to do each thing, generators over hand-written
187
186
  files, imports that never cross a surface boundary, a stable error code with a runnable \`fix:\` for
188
- every new failure, and \`bun run check\` green as the last line of *Done when*.
187
+ every new failure, and \`bun run verify\` green as the last line of *Done when*.
189
188
 
190
189
  ## Output
191
190
 
@@ -202,18 +201,16 @@ allowed-tools: Read, Write, Edit, Glob, Grep, Bash
202
201
 
203
202
  # /verify
204
203
 
205
- Run \`bun run check\`.
204
+ Run \`bun run verify\`.
206
205
 
207
- It is \`x build --target static\` and then \`x verify\`, and it is THE gate — the one CI runs
208
- (\`.github/workflows/ci.yml\`) and the one \`README.md\` names. \`x verify\` alone is half of it:
209
- the \`budgets\` step measures \`.x/build-stats.json\`, which only the build writes, so a bare
210
- \`x verify\` reports X_BUDGET_UNMEASURED on a tree with nothing wrong with it.
206
+ It is \`bunx x verify\`, and it is THE gate — the one CI runs (\`.github/workflows/ci.yml\`) and the
207
+ one \`README.md\` names. Its \`budgets\` step builds what it weighs, so nothing runs before it.
211
208
 
212
209
  Green: say so and stop.
213
210
 
214
211
  Red: fix every finding, then re-run until green. Each finding carries a stable code, a cause and an
215
- executable \`fix:\` — **run the \`fix:\` verbatim before improvising**, and use \`x errors explain <CODE>\`
216
- when the cause is not enough. \`bun run check --json\` gives the same steps machine-readably; \`x doctor\`
212
+ executable \`fix:\` — **run the \`fix:\` verbatim before improvising**, and use \`bunx x errors explain <CODE>\`
213
+ when the cause is not enough. \`bun run verify --json\` gives the same steps machine-readably; \`bunx x doctor\`
217
214
  covers the case where the environment, not the code, is what is broken.
218
215
 
219
216
  Do not narrow the gate to make it pass. There is no \`--only\` and no \`--skip\`; disabling a lint rule,
@@ -9,6 +9,7 @@ import { claudeCommandFiles } from './scaffold-claude-commands';
9
9
 
10
10
  /**
11
11
  * Read-only commands and the two write commands whose blast radius is a file the gate checks.
12
+ * Spelled `bunx x`, the form that runs with no global `x` and the one the generators print.
12
13
  * Deliberately absent: `x db reset`, `x db backfill --write`, `x secrets`, `x deploy` — each is
13
14
  * destructive or reaches production, and an allowlist that covers them is a prompt nobody reads.
14
15
  *
@@ -23,26 +24,28 @@ const settings = (): string => `{
23
24
  "Bash(bun test:*)",
24
25
  "Bash(bun run typecheck)",
25
26
  "Bash(bun run lint)",
27
+ "Bash(bun run verify:*)",
26
28
  "Bash(bunx biome check:*)",
27
- "Bash(x verify:*)",
28
- "Bash(x test:*)",
29
- "Bash(x doctor:*)",
30
- "Bash(x g:*)",
31
- "Bash(x routes:*)",
32
- "Bash(x actions:*)",
33
- "Bash(x queries:*)",
34
- "Bash(x entities:*)",
35
- "Bash(x jobs list:*)",
36
- "Bash(x jobs show:*)",
37
- "Bash(x tasks:*)",
38
- "Bash(x policy:*)",
39
- "Bash(x i18n check:*)",
40
- "Bash(x errors:*)",
41
- "Bash(x env check:*)",
42
- "Bash(x manifest:*)",
43
- "Bash(x db gen:*)",
44
- "Bash(x db migrate:*)",
45
- "Bash(x db branch:*)",
29
+ "Bash(bunx x verify:*)",
30
+ "Bash(bunx x test:*)",
31
+ "Bash(bunx x doctor:*)",
32
+ "Bash(bunx x g:*)",
33
+ "Bash(bunx x routes:*)",
34
+ "Bash(bunx x actions:*)",
35
+ "Bash(bunx x queries:*)",
36
+ "Bash(bunx x entities:*)",
37
+ "Bash(bunx x jobs list:*)",
38
+ "Bash(bunx x jobs show:*)",
39
+ "Bash(bunx x tasks:*)",
40
+ "Bash(bunx x policy:*)",
41
+ "Bash(bunx x i18n check:*)",
42
+ "Bash(bunx x errors:*)",
43
+ "Bash(bunx x docs:*)",
44
+ "Bash(bunx x env check:*)",
45
+ "Bash(bunx x manifest:*)",
46
+ "Bash(bunx x db gen:*)",
47
+ "Bash(bunx x db migrate:*)",
48
+ "Bash(bunx x db branch:*)",
46
49
  "Bash(git status)",
47
50
  "Bash(git diff:*)",
48
51
  "Bash(git log:*)"
@@ -68,7 +71,7 @@ const readme = (app: NameSet): string => `# .claude/
68
71
 
69
72
  What Claude Code reads when it works on ${app.kebab}. Written by \`x new\`, owned by you from the
70
73
  moment it lands: edit any file, or delete any file, and nothing in the app breaks. Nothing here is
71
- enforced by \`x verify\`, and nothing here reaches outside this repository.
74
+ enforced by \`bunx x verify\`, and nothing here reaches outside this repository.
72
75
 
73
76
  One thing in it does *write*: the \`PostToolUse\` hook in \`settings.json\` reformats each file an agent
74
77
  edits, in place, the moment it is saved. That is the only thing here that touches your code on its
@@ -86,7 +89,7 @@ So the whole bundle is three description lines of standing cost. The rest is pai
86
89
 
87
90
  | File | For |
88
91
  |---|---|
89
- | \`commands/feature.md\` | \`/feature\` — build or fix one thing end to end, gated on \`x verify\` |
92
+ | \`commands/feature.md\` | \`/feature\` — build or fix one thing end to end, gated on \`bun run verify\` |
90
93
  | \`commands/planx.md\` | \`/planx\` — write a plan to \`docs/plans/\` for another agent to execute |
91
94
  | \`commands/verify.md\` | \`/verify\` — run the gate and fix what it reports |
92
95
  | \`agents/shape.md\` | the idea-stage pass: which primitive, which slice, or "do not build this" |
@@ -99,7 +102,7 @@ a line it may not cross. That is what makes two of them safe to run at once.
99
102
 
100
103
  ## What is deliberately not here
101
104
 
102
- **No command that wraps \`x g\`.** \`x g <kind> <name>\` already is that command, and \`x g --help\` is
105
+ **No command that wraps \`bunx x g\`.** \`bunx x g <kind> <name>\` already is that command, and \`bunx x g --help\` is
103
106
  the only list of kinds — a slash command restating it is a second copy that drifts the first time a
104
107
  kind is added.
105
108
 
@@ -113,7 +116,7 @@ means it rewrites that file in place** — the same safe fixes \`biome check --w
113
116
  repo-wide, but your working tree does change after every agent edit. Scoped to one file, so it costs
114
117
  milliseconds, and it settles formatting arguments before they reach a diff.
115
118
 
116
- It is not a typecheck, because a *scoped* one does not exist: \`tsc\` needs the project, and \`x verify\`
119
+ It is not a typecheck, because a *scoped* one does not exist: \`tsc\` needs the project, and \`bunx x verify\`
117
120
  takes no \`--only\` and no \`--skip\` by design — narrowing the gate would make "green" mean whatever
118
121
  the caller chose. If you want types on every edit, \`bun run typecheck\` is the whole project and you
119
122
  are choosing to pay for it.
@@ -3,6 +3,7 @@
3
3
  // `x deploy --method compose` runs, and the single-node rung of the scale ladder.
4
4
 
5
5
  import type { NameSet } from './naming';
6
+ import { RUNTIME_UID } from './scaffold-helm';
6
7
 
7
8
  /**
8
9
  * The production env file, relative to the app root. Named once because two readers must agree:
@@ -19,6 +20,8 @@ export const composeProdFile = (
19
20
  #
20
21
  # IMAGE=ghcr.io/you/${app.kebab}:1.2.3 x deploy --image ghcr.io/you/${app.kebab}:1.2.3
21
22
  #
23
+ # ${PROD_ENV_FILE} first: \`x env init --production\` writes every value below with fresh secrets.
24
+ #
22
25
  # By hand, always with \`--env-file ${PROD_ENV_FILE}\`: Compose fills \`\${VAR:?…}\` below from the shell
23
26
  # and \`--env-file\` only, NEVER from \`env_file:\`, so without it a value set only in that file is
24
27
  # "missing" and the parse fails. \`x deploy\` passes it on every step.
@@ -55,8 +58,10 @@ x-image: &image
55
58
  # filesystem, no Linux capabilities (nothing here binds below 1024), no setuid escalation, and a
56
59
  # memory ceiling so one runaway role takes down its container rather than the box. /tmp and .x/
57
60
  # are tmpfs — .x/ is where a still-embedded binding keeps state, which production never relies on.
61
+ # A tmpfs mounts root-owned and hides the image's chown, so .x/ names the image's USER: without
62
+ # it every role refuses at boot (X_STORAGE_UNWRITABLE, EACCES mkdir '/app/.x/storage').
58
63
  read_only: true
59
- tmpfs: [/tmp, /app/.x]
64
+ tmpfs: [/tmp, '/app/.x:uid=${RUNTIME_UID},gid=${RUNTIME_UID}']
60
65
  cap_drop: [ALL]
61
66
  security_opt: ['no-new-privileges:true']
62
67
  mem_limit: 1g
@@ -121,6 +121,10 @@ ENTRYPOINT ["bun", "apps/web/server.ts"]
121
121
  * has ever produced ignoring a filename nothing writes, which reads as a rule still in force.
122
122
  * `scripts/image-contract.ts` holds every ignore file IN THIS TREE to the same line; a generated
123
123
  * app's copy is this template's, and `scaffold-container.test.ts` is where it is judged.
124
+ *
125
+ * Tests and the root `docs/` go for a different reason: `COPY . .` sits above the prebuilt step, so
126
+ * an edit to a file no boot reads reran Babel and Sass. No `*.fixture.ts` line: non-test source can
127
+ * reach a fixture, and a missing module at boot is a worse failure than a few kilobytes.
124
128
  */
125
129
  const dockerignore = (): string => `**/.env
126
130
  **/.env.*
@@ -143,6 +147,11 @@ node_modules
143
147
  coverage
144
148
  **/test-results
145
149
  **/playwright-report
150
+ # Tests run in CI, never in a container, and an edit to one would rerun every line below COPY . .
151
+ **/*.test.ts
152
+ **/*.test.tsx
153
+ # Anchored: a package may hold a docs/ it reads at run time. See "What the image leaves out" in README.
154
+ docs
146
155
  `;
147
156
 
148
157
  const readme = (app: NameSet): string => `# docker
@@ -175,6 +184,12 @@ compiled stylesheet is made once, there, by the image's own Bun — so a pod bui
175
184
  Without that line every boot runs Babel and Sass over the whole app and logs
176
185
  \`X_IMAGE_NOT_PREBUILT\`, naming it.
177
186
 
187
+ ### What the image leaves out
188
+
189
+ \`docker/Dockerfile.dockerignore\` drops \`*.test.ts\`, \`*.test.tsx\` and the root \`docs/\`: no boot
190
+ reads them, and an edit to one leaves the prebuilt layer cached. An app that serves a file from
191
+ \`docs/\` re-includes it with \`!docs/<path>\`, on a line after \`docs\`.
192
+
178
193
  ## One box, every role
179
194
 
180
195
  \`\`\`sh
@@ -276,10 +291,15 @@ somebody else's repository is a command that cannot run. \`--image\` sets \`imag
276
291
  | Job | release | \`ROLE=migrate\`, a \`pre-install,pre-upgrade\` hook — it runs to completion first |
277
292
  | Ingress | release | off by default; routes \`/_x/sync\` to sync and \`/\` to web |
278
293
  | HorizontalPodAutoscaler | role, opt-in | rps, websocket connections, queue depth — never CPU |
279
-
280
- No ServiceMonitor and no PodDisruptionBudget: the first needs a CRD \`helm install\` fails on in a
281
- cluster with no Prometheus operator, and both are cluster policy rather than this app's topology.
282
- Every role already answers \`/metrics\` on \`metricsPort\`.
294
+ | ServiceMonitor | release | scrapes \`/metrics\` on \`metricsPort\`, every serving role |
295
+ | PrometheusRule | release | stalled queue, dead letters, lost leases, dropped frames, 5xx share, target down |
296
+
297
+ The last two render only where the cluster serves the Prometheus operator's kinds
298
+ (\`serviceMonitor.enabled\` and \`alerts.enabled\` are \`auto\`), so \`helm install\` still succeeds on a
299
+ cluster without it; \`true\` forces them, \`false\` declines. Each rule's threshold, \`for\` and
300
+ severity are values under \`alerts.rules.<alert>\`. Outside Kubernetes, \`x g chart-alerts --print\`
301
+ prints the same rules for any Prometheus rule loader. No PodDisruptionBudget: that is cluster
302
+ policy rather than this app's topology.
283
303
  `;
284
304
 
285
305
  /** The container files for a new app, in the order a reader meets them. */
@@ -203,7 +203,7 @@ afterAll(clearRoutes);
203
203
 
204
204
  unitTest('the dashboard is gated, renders per request, and hydrates one island', async () => {
205
205
  const view = await renderRoute(page, { url });
206
- expect(page.config.render).toBe('ssr');
206
+ expect(page.config.render).toBe('stream');
207
207
  expect(page.config.policy?.permission).toBe('dashboard:read');
208
208
  expect(page.config.offline).toBe('runtime');
209
209
  expect(page.config.budget.js).toBe('60kb');
@@ -230,12 +230,12 @@ unitTest('the table is the route table: path, surface and render mode per route'
230
230
  expect([...row.matchAll(/([^>]*)<\\/td>/g)].map((cell) => cell[1])).toEqual([
231
231
  '/dashboard',
232
232
  'app',
233
- 'ssr',
233
+ 'stream',
234
234
  ]);
235
235
  expect(view.text).toContain(t('app.dashboard.routesCaption'));
236
236
  expect(view.meta.title).toBe(t('app.dashboard.title'));
237
237
  // The sidebar knows where it is.
238
- expect(view.html).toMatch(/<a href="\\/dashboard" aria-current="page"/);
238
+ expect(view.html).toMatch(/<a [^>]*href="\\/dashboard" aria-current="page"/);
239
239
  });
240
240
  `;
241
241
 
@@ -12,6 +12,8 @@ export const exampleViewTest =
12
12
  import { expect, unitTest } from '@ultimat3/testing';
13
13
  import {
14
14
  bucketByDay,
15
+ CHART_MAX_DAYS,
16
+ chartDays,
15
17
  formatCount,
16
18
  type PostRow,
17
19
  postStats,
@@ -43,9 +45,14 @@ unitTest('postStats counts the week, compares it with the prior one, and counts
43
45
  expect(postStats([...rows, row('d', 0.25)], NOW).today).toBe(1);
44
46
  });
45
47
 
46
- unitTest('a zero baseline yields no delta, and no rows count nothing', () => {
47
- expect(postStats([row('a', 1)], NOW).delta).toBeUndefined();
48
- expect(postStats([], NOW)).toEqual({ total: 0, lastWeek: 0, delta: undefined, today: 0 });
48
+ unitTest('from a zero baseline the chip is the difference, and no rows count nothing', () => {
49
+ expect(postStats([row('a', 1)], NOW).delta).toEqual({ text: '+1', trend: 'up' });
50
+ expect(postStats([], NOW)).toEqual({
51
+ total: 0,
52
+ lastWeek: 0,
53
+ delta: { text: '0', trend: 'flat' },
54
+ today: 0,
55
+ });
49
56
  });
50
57
 
51
58
  unitTest('bucketByDay is one bar per day, oldest first, quiet days included', () => {
@@ -53,12 +60,27 @@ unitTest('bucketByDay is one bar per day, oldest first, quiet days included', ()
53
60
  expect(points).toHaveLength(14);
54
61
  expect(points.at(-1)?.key).toBe('03-15');
55
62
  expect(points[0]?.key).toBe('03-02');
56
- expect(points.reduce((sum, point) => sum + point.value, 0)).toBe(3);
63
+ expect(points.reduce((sum, point) => sum + (point.value ?? 0), 0)).toBe(3);
57
64
  // Eight days ago is 03-07: inside a 14-day window, so the bar is there.
58
65
  expect(points.find((point) => point.key === '03-07')?.value).toBe(1);
59
66
  expect(bucketByDay([], NOW, 3).map((point) => point.value)).toEqual([0, 0, 0]);
60
67
  });
61
68
 
69
+ unitTest('a capped read leaves the days before its oldest row unknown — null, never 0', () => {
70
+ const values = bucketByDay([row('a', 1), row('b', 3)], NOW, 5, { capped: true }).map(
71
+ (point) => point.value,
72
+ );
73
+ // 03-11 is before the oldest row the capped read saw (03-12): unknown. 03-13 is a quiet day: 0.
74
+ expect(values).toEqual([null, 1, 0, 1, 0]);
75
+ });
76
+
77
+ unitTest('chartDays is the window in whole days, never zero and never past the cap', () => {
78
+ expect(chartDays(new Date(NOW.getTime() - 7 * DAY_MS), NOW)).toBe(7);
79
+ expect(chartDays(new Date(NOW.getTime() - DAY_MS / 2), NOW)).toBe(1);
80
+ // A URL may ask for ten years: the chart draws at most this many bars.
81
+ expect(chartDays(new Date(NOW.getTime() - 3650 * DAY_MS), NOW)).toBe(CHART_MAX_DAYS);
82
+ });
83
+
62
84
  unitTest('toPostRow serialises a Date, passes ISO text through, and keeps the price whole', () => {
63
85
  const at = new Date('2026-01-02T03:04:05.000Z');
64
86
  const price = { minor: 1900, currency: 'USD' };
@@ -139,7 +161,7 @@ afterEach(() => {
139
161
 
140
162
  unitTest('the dashboard is gated, renders per request, and hydrates one island', async () => {
141
163
  const view = await renderRoute(page, { url, actor: viewer });
142
- expect(page.config.render).toBe('ssr');
164
+ expect(page.config.render).toBe('stream');
143
165
  expect(page.config.policy?.permission).toBe('dashboard:read');
144
166
  expect(page.config.offline).toBe('runtime');
145
167
  expect(page.config.budget.js).toBe('60kb');
@@ -169,8 +191,11 @@ unitTest('it counts and lists the posts of the org that is looking', async () =>
169
191
  expect(view.html.match(/data-segment="/g)).toHaveLength(2);
170
192
  expect(view.html).toContain('role="meter"');
171
193
  expect(view.text).toContain('This week');
194
+ // The chart's window is the default two weeks, and every preset is a link.
195
+ expect(view.html).toContain('href="/dashboard?range=week"');
196
+ expect(view.html).toContain('href="/dashboard?range=month"');
172
197
  // The sidebar knows where it is.
173
- expect(view.html).toMatch(/<a href="\\/dashboard" aria-current="page"/);
198
+ expect(view.html).toMatch(/<a [^>]*href="\\/dashboard" aria-current="page"/);
174
199
  });
175
200
 
176
201
  unitTest('another org sees none of them: the page says so instead of an empty table', async () => {
@@ -185,6 +210,18 @@ unitTest('another org sees none of them: the page says so instead of an empty ta
185
210
  expect(view.meta.description).toBe(t('app.dashboard.description'));
186
211
  });
187
212
 
213
+ unitTest('the chart window is read from the URL and survives as a link', async () => {
214
+ await post('This week', 3);
215
+ const week = await renderRoute(page, { url: \`\${url}?range=week\`, actor: viewer });
216
+ expect(week.data.url).toBe(\`\${url}?range=week\`);
217
+ // Seven bars for the week; the default window draws fourteen.
218
+ const keys = (html: string): number =>
219
+ html.match(/<th[^>]*scope="row"[^>]*>\\d{2}-\\d{2}</g)?.length ?? 0;
220
+ const fortnight = await renderRoute(page, { url, actor: viewer });
221
+ expect(keys(week.html)).toBe(7);
222
+ expect(keys(fortnight.html)).toBe(14);
223
+ });
224
+
188
225
  unitTest('a viewer the post read refuses is refused, never shown the org’s posts', async () => {
189
226
  await post('Not for them', 0);
190
227
  // In the org, onto the page, without \`post:read\`: the query's policy decides, not the page's.
@@ -195,14 +232,17 @@ unitTest('a viewer the post read refuses is refused, never shown the org’s pos
195
232
  });
196
233
 
197
234
  unitTest('the build measures the empty page: no database is not a failure there', async () => {
198
- const measured = await page.dashboardData(() => Promise.reject(dbUnavailable('no database')));
235
+ const measured = await page.dashboardData(
236
+ () => Promise.reject(dbUnavailable('no database')),
237
+ url,
238
+ );
199
239
  // The clock is frozen under test, so the instant the windows are cut at is this one.
200
- expect(measured).toEqual({ rows: [], now: new Date().toISOString() });
240
+ expect(measured).toEqual({ rows: [], now: new Date().toISOString(), url });
201
241
  });
202
242
 
203
243
  unitTest('any other failure of the read propagates', async () => {
204
244
  const cause = new TypeError('the read broke');
205
245
  const read = (): Promise<never> => Promise.reject(cause);
206
- expect(await page.dashboardData(read).catch((error: unknown) => error)).toBe(cause);
246
+ expect(await page.dashboardData(read, url).catch((error: unknown) => error)).toBe(cause);
207
247
  });
208
248
  `;
@@ -0,0 +1,156 @@
1
+ // The `--example` dashboard's view module, `dashboard-view.ts`: the pure numbers behind the page —
2
+ // stats, per-day buckets, the chart's window presets and its day bound. Split from
3
+ // `scaffold-dashboard-example.ts`, which keeps the page that reads it.
4
+
5
+ export const exampleView =
6
+ (): string => `// The dashboard's numbers, apart from its markup: pure functions over the rows the page loaded,
7
+ // so the server render and a test cannot disagree about a figure. No I/O, no \`t()\`, no JSX.
8
+
9
+ import type { Money } from '@ultimat3/money';
10
+ import { type ChartPoint, deltaOf, type StatDelta } from '@ultimat3/ui';
11
+
12
+ /**
13
+ * The chart's windows, shortest first. The list is this app's — "last 24 hours" or "this quarter"
14
+ * is a business decision — and \`timeRangeUrl\` is the one rule that reads it from the URL.
15
+ */
16
+ export const CHART_RANGES = [
17
+ { id: 'week', days: 7 },
18
+ { id: 'fortnight', days: 14 },
19
+ { id: 'month', days: 30 },
20
+ ] as const;
21
+
22
+ export const DEFAULT_CHART_RANGE = 'fortnight';
23
+
24
+ /** The chart buckets UTC days, so a custom window's wall-clock times are UTC too: one zone. */
25
+ export const CHART_ZONE = 'UTC';
26
+
27
+ /** The most bars a custom window draws: one per day, so this bounds the markup a URL can ask for. */
28
+ export const CHART_MAX_DAYS = 92;
29
+
30
+ const DAY_MS = 86_400_000;
31
+
32
+ /** Whole days a window covers, at least one and at most \`CHART_MAX_DAYS\`. */
33
+ export function chartDays(from: Date, to: Date): number {
34
+ const days = Math.ceil((to.getTime() - from.getTime()) / DAY_MS);
35
+ return Math.min(CHART_MAX_DAYS, Math.max(1, days));
36
+ }
37
+
38
+ /** One post as the page reads it. \`createdAt\` is ISO text: a \`Date\` cannot cross a JSON seam. */
39
+ export interface PostRow {
40
+ readonly id: string;
41
+ readonly title: string;
42
+ /** Integer minor units plus an ISO code, never a float — the entity's \`money()\` column. */
43
+ readonly price: Money;
44
+ readonly createdAt: string;
45
+ }
46
+
47
+ /**
48
+ * Duck-typed rather than the entity's own row: the page only ever reads these four columns. The
49
+ * query reads through the typed handle, so \`createdAt\` arrives as the \`Date\` the entity declares.
50
+ */
51
+ export function toPostRow(row: {
52
+ readonly id: string;
53
+ readonly title: string;
54
+ readonly price: Money;
55
+ readonly createdAt: Date | string;
56
+ }): PostRow {
57
+ const at = row.createdAt;
58
+ return {
59
+ id: row.id,
60
+ title: row.title,
61
+ price: row.price,
62
+ createdAt: at instanceof Date ? at.toISOString() : at,
63
+ };
64
+ }
65
+
66
+ /** Free (a zero price) against paid, over every row loaded — the two halves of the ring. */
67
+ export interface PriceMix {
68
+ readonly free: number;
69
+ readonly paid: number;
70
+ }
71
+
72
+ export function priceMix(rows: readonly PostRow[]): PriceMix {
73
+ const paid = rows.filter((row) => row.price.minor > 0).length;
74
+ return { free: rows.length - paid, paid };
75
+ }
76
+
77
+ /**
78
+ * The dearest price per currency, in minor units — what each row's in-cell bar is a share of.
79
+ * Per currency because 1,900 cents and 1,900 yen are not the same height.
80
+ */
81
+ export function priceCeilings(rows: readonly PostRow[]): ReadonlyMap<string, number> {
82
+ const ceilings = new Map<string, number>();
83
+ for (const row of rows) {
84
+ const { currency, minor } = row.price;
85
+ ceilings.set(currency, Math.max(ceilings.get(currency) ?? 0, minor));
86
+ }
87
+ return ceilings;
88
+ }
89
+
90
+ export interface PostStats {
91
+ readonly total: number;
92
+ /** Created in the seven days ending at \`now\`. */
93
+ readonly lastWeek: number;
94
+ /** \`lastWeek\` against the seven days before it: a percentage, or the difference from zero. */
95
+ readonly delta: StatDelta | undefined;
96
+ /** Created in the UTC day \`now\` falls in — a partial day, so it carries no delta. */
97
+ readonly today: number;
98
+ }
99
+
100
+ const createdAtOf = (row: PostRow): number => Date.parse(row.createdAt);
101
+
102
+ const within = (rows: readonly PostRow[], from: number, to: number): number =>
103
+ rows.filter((row) => {
104
+ const at = createdAtOf(row);
105
+ return Number.isFinite(at) && at > from && at <= to;
106
+ }).length;
107
+
108
+ export function postStats(rows: readonly PostRow[], now: Date): PostStats {
109
+ const end = now.getTime();
110
+ const weekAgo = end - 7 * DAY_MS;
111
+ const dayStart = Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate());
112
+ const lastWeek = within(rows, weekAgo, end);
113
+ return {
114
+ total: rows.length,
115
+ lastWeek,
116
+ delta: deltaOf(lastWeek, within(rows, weekAgo - 7 * DAY_MS, weekAgo)),
117
+ today: within(rows, dayStart - 1, end),
118
+ };
119
+ }
120
+
121
+ /** \`MM-DD\` of a UTC day. ISO, never \`toLocaleDateString\`: the chart's axis is data, not prose. */
122
+ const dayKey = (at: number): string => new Date(at).toISOString().slice(5, 10);
123
+
124
+ /**
125
+ * One bucket per UTC day, the last one being the day \`now\` falls in, oldest first — the shape
126
+ * \`BarChart\` draws. Every day is present even when nothing was created on it: a chart with the
127
+ * quiet days removed reads as busier than the app is. A quiet day is 0; a day the read could not
128
+ * see — before the oldest row of a \`capped\` read — is \`null\`, a gap, never a zero.
129
+ */
130
+ export function bucketByDay(
131
+ rows: readonly PostRow[],
132
+ now: Date,
133
+ days: number,
134
+ options: { readonly capped?: boolean } = {},
135
+ ): readonly ChartPoint[] {
136
+ const today = Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate());
137
+ const start = today - (days - 1) * DAY_MS;
138
+ const counts: number[] = Array.from({ length: days }, () => 0);
139
+ for (const row of rows) {
140
+ const at = createdAtOf(row);
141
+ if (!Number.isFinite(at)) continue;
142
+ const index = Math.floor((at - start) / DAY_MS);
143
+ if (index >= 0 && index < days) counts[index] = (counts[index] ?? 0) + 1;
144
+ }
145
+ const oldest = Math.min(...rows.map(createdAtOf).filter(Number.isFinite));
146
+ const seenFrom = options.capped === true ? Math.floor((oldest - start) / DAY_MS) : 0;
147
+ return counts.map((value, index) => ({
148
+ key: dayKey(start + index * DAY_MS),
149
+ value: index < seenFrom ? null : value,
150
+ }));
151
+ }
152
+
153
+ /** A figure for a tile, in the page's locale. \`Intl.NumberFormat\` needs no time zone. */
154
+ export const formatCount = (value: number, locale: string): string =>
155
+ new Intl.NumberFormat(locale).format(value);
156
+ `;