@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
@@ -18,28 +18,32 @@ const domainIndex = (
18
18
  ): string => `// Pure types and constants. No I/O of any kind: no fs, no network, no database, no env reads.
19
19
  // \`Money\` is @ultimat3/schema's, re-exported and never restated: one declaration of
20
20
  // { minor, currency } is what makes an entity column, a form field and this module the same type.
21
- import { UltimateError } from '@ultimat3/core';
21
+ import { defineError } from '@ultimat3/core';
22
22
  import type { MoneyValue as Money } from '@ultimat3/schema';
23
23
 
24
24
  export type { MoneyValue as Money } from '@ultimat3/schema';
25
25
 
26
+ interface CurrencyPair {
27
+ readonly left: string;
28
+ readonly right: string;
29
+ }
30
+
26
31
  /**
27
32
  * Never a bare Error: an agent reading the failure needs the code, the cause and the exact command
28
33
  * that resolves it. Adding across currencies is a bug in the caller's data, not a runtime hiccup,
29
- * so the fix names the conversion that has to happen first.
30
- *
31
- * No \`docs:\`: \`UltimateError\` resolves the link from the code's registered descriptor, so it has
32
- * one home. A per-code URL written here is a page that does not exist.
34
+ * so it answers 422 and the fix names the conversion that has to happen first. One declaration
35
+ * — \`defineError\` — holds the title, the status and the class; \`guards/error-declared.ts\` keeps
36
+ * it the only way. No \`docs:\`: the link resolves from the code's registered descriptor.
33
37
  */
34
- export class ${app.pascal}CurrencyMismatchError extends UltimateError {
35
- constructor(input: { readonly left: string; readonly right: string }) {
36
- super({
37
- code: '${currencyCode(app)}',
38
- cause: \`cannot add \${input.left} to \${input.right}: Money is only additive inside one currency\`,
39
- fix: \`bun test packages/domain/src/index.test.ts # add() is defined only inside one currency — convert \${input.right} to \${input.left} before calling it\`,
40
- });
41
- }
42
- }
38
+ export const ${app.pascal}CurrencyMismatchError = defineError<CurrencyPair>({
39
+ code: '${currencyCode(app)}',
40
+ title: 'money added across two currencies',
41
+ status: 422,
42
+ cause: (pair) =>
43
+ \`cannot add \${pair.left} to \${pair.right}: Money is only additive inside one currency\`,
44
+ fix: (pair) =>
45
+ \`bun test packages/domain/src/index.test.ts # add() is defined only inside one currency — convert \${pair.right} to \${pair.left} before calling it\`,
46
+ });
43
47
 
44
48
  export const zero = (currency: string): Money => ({ minor: 0, currency });
45
49
 
@@ -91,8 +91,10 @@ const origin = Bun.env.SITE_ORIGIN;
91
91
  * from the CDN and from the container.
92
92
  *
93
93
  * A DYNAMIC route contributes exactly the URLs this build emitted for it, read back off the report:
94
- * the sitemap then cannot name a page the artifact does not contain. \`prerender()\` is asked again
95
- * only for what each page says of itself — its \`lastmod\`, the \`locales\` it is listed in.
94
+ * the sitemap then cannot name a page the artifact does not contain. What each page says of itself —
95
+ * its \`lastmod\`, the \`locales\` it is listed in — is \`report.entries\`: the build's own
96
+ * \`prerender()\` answers, so a route that lists its pages off the app's queries is never asked again
97
+ * once the build has stopped answering them.
96
98
  *
97
99
  * With \`seo.sitemap.sections\` the answer is an index and its parts: \`/sitemap.xml\` plus
98
100
  * \`/sitemaps/<name>-<n>.xml\`, every one written here.
@@ -110,9 +112,12 @@ async function writeSeoFiles(report: PrerenderReport): Promise<readonly string[]
110
112
  disallow: settings.disallow,
111
113
  robotsGroups: settings.robotsGroups,
112
114
  sitemap: settings.sitemap,
115
+ llms: settings.llms,
113
116
  root,
114
117
  pagesFor: (route) =>
115
118
  report.pages.filter((page) => page.route === route).map((page) => page.path),
119
+ entries: report.entries,
120
+ llmsPages: report.llmsPages,
116
121
  });
117
122
  return await writeSiteSeo(out, seo);
118
123
  }
@@ -0,0 +1,37 @@
1
+ // `apps/web/exercise.ts`: the app's overrides for `x exercise`, empty. Written by `x new` so the
2
+ // file every `X_CONTRACT_EXERCISE_UNREACHED` fix names exists before the first line is pasted in.
3
+
4
+ import type { GeneratedFile } from './naming';
5
+
6
+ const exercise =
7
+ (): string => `// What \`x exercise\` cannot guess about this app: an input a primitive needs bound to a real
8
+ // row, an actor its policy permits, or a primitive that must not run in the pass and why. Empty
9
+ // runs every action, query and job as generated. \`x exercise --json\` prints the entry to paste
10
+ // for each one it could not reach; each value is the \`exercise\` option \`.contract()\` takes.
11
+ import { defineExercise } from '@ultimat3/testing';
12
+
13
+ export const exercise = defineExercise({});
14
+ `;
15
+
16
+ // The import IS the check: `defineExercise` refuses a blank `off` as the module loads, so a reason
17
+ // deleted from an entry fails `bun test` here, before the gate's `exercise` step runs at all.
18
+ const exerciseTest =
19
+ (): string => `// The overrides load, and every primitive they turn off says why.
20
+ import { expect, unitTest } from '@ultimat3/testing';
21
+ import { exercise } from './exercise';
22
+
23
+ unitTest('apps/web/exercise.ts loads, and every off entry names its reason', () => {
24
+ for (const [name, entry] of exercise.entries) {
25
+ if ('off' in entry) {
26
+ expect({ name, blank: entry.off.trim() === '' }).toEqual({ name, blank: false });
27
+ }
28
+ }
29
+ expect(exercise.entries).toBeInstanceOf(Map);
30
+ });
31
+ `;
32
+
33
+ /** Written by `x new`, with or without the example slice: the gate's `exercise` step reads it. */
34
+ export const exerciseFiles = (): readonly GeneratedFile[] => [
35
+ { path: 'apps/web/exercise.ts', contents: exercise() },
36
+ { path: 'apps/web/exercise.test.ts', contents: exerciseTest() },
37
+ ];
@@ -18,15 +18,23 @@
18
18
  // design system's rules a scaffolded app's gate held.
19
19
  //
20
20
  // And one is about DATA: a `repo.ts` reads through the typed handle. It ships because the generator
21
- // used to emit string SQL, and every app that started from it copied the shape.
21
+ // used to emit string SQL, and every app that started from it copied the shape. One is about the
22
+ // NETWORK: a server `fetch(` carries a signal, or is `outbound()` from `@ultimat3/core/outbound`. `error-declared`
23
+ // is `bare-error`'s second half: an app code is declared once, with core's `defineError`. One is
24
+ // about the TAB: an island polls with `visibleInterval()`, which a hidden tab and a lost session stop.
25
+ // One is about the QUESTION: a destructive act asks through `ConfirmDialog`, never `confirm(`.
22
26
 
23
27
  import { animatedLayoutPropertyGuardFiles } from './guard-animated-layout-property';
24
28
  import { bareErrorGuardFiles } from './guard-bare-error';
29
+ import { errorDeclaredGuardFiles } from './guard-error-declared';
25
30
  import { focusVisibleGuardFiles } from './guard-focus-visible';
26
31
  import { imageDimensionsGuardFiles } from './guard-image-dimensions';
27
32
  import { islandWithoutStatesGuardFiles } from './guard-island-without-states';
33
+ import { nativeConfirmGuardFiles } from './guard-native-confirm';
28
34
  import { rawBreakpointGuardFiles } from './guard-raw-breakpoint';
29
35
  import { rawColourGuardFiles } from './guard-raw-colour';
36
+ import { rawFetchGuardFiles } from './guard-raw-fetch';
37
+ import { rawIntervalGuardFiles } from './guard-raw-interval';
30
38
  import { rawLengthGuardFiles } from './guard-raw-length';
31
39
  import { rawMotionGuardFiles } from './guard-raw-motion';
32
40
  import { rawShadowGuardFiles } from './guard-raw-shadow';
@@ -48,11 +56,15 @@ import type { GeneratedFile } from './naming';
48
56
  const SHIPPED: readonly (readonly [string, () => readonly GeneratedFile[]])[] = [
49
57
  ['animated-layout-property', animatedLayoutPropertyGuardFiles],
50
58
  ['bare-error', bareErrorGuardFiles],
59
+ ['error-declared', errorDeclaredGuardFiles],
51
60
  ['focus-visible', focusVisibleGuardFiles],
52
61
  ['image-dimensions', imageDimensionsGuardFiles],
53
62
  ['island-without-states', islandWithoutStatesGuardFiles],
63
+ ['native-confirm', nativeConfirmGuardFiles],
54
64
  ['raw-breakpoint', rawBreakpointGuardFiles],
55
65
  ['raw-colour', rawColourGuardFiles],
66
+ ['raw-fetch', rawFetchGuardFiles],
67
+ ['raw-interval', rawIntervalGuardFiles],
56
68
  ['raw-length', rawLengthGuardFiles],
57
69
  ['raw-motion', rawMotionGuardFiles],
58
70
  ['raw-shadow', rawShadowGuardFiles],
@@ -0,0 +1,234 @@
1
+ // The monitoring objects every Ultimate chart ships: the scrape object and the alert rules. The
2
+ // rules are one table, rendered as the chart's PrometheusRule template and as plain rule groups for
3
+ // a loader outside Kubernetes. Only mechanism ships — a stalled queue, a dead letter, a lost lease,
4
+ // a dropped frame, a 5xx share, a dead target; a latency target is the app's (axiom 8).
5
+ import type { GeneratedFile, NameSet } from './naming';
6
+
7
+ /** How an expression names a series and groups its result, per renderer. */
8
+ export interface AlertScope {
9
+ /** `name{matchers}` — the scope's own matchers, then `extra`. */
10
+ readonly series: (name: string, extra?: string) => string;
11
+ /** The labels every rule groups by, so one release (or one job) fires one alert. */
12
+ readonly by: string;
13
+ /** The label naming a scrape target's role. */
14
+ readonly component: string;
15
+ /** The rule's threshold, as the renderer spells it: a value lookup or the default. */
16
+ readonly threshold: (fallback: number) => string;
17
+ }
18
+
19
+ export interface ChartAlert {
20
+ readonly alert: string;
21
+ readonly expr: (scope: AlertScope) => string;
22
+ /** The default `alerts.rules.<alert>.threshold`, for a rule whose comparison is a number. */
23
+ readonly threshold?: number;
24
+ readonly for: string;
25
+ readonly severity: 'warning' | 'critical';
26
+ readonly summary: string;
27
+ /** The heading in `docs/ops/06-runbooks.md` the rule's `runbook_url` lands on. */
28
+ readonly anchor: string;
29
+ }
30
+
31
+ const rule = (alert: Omit<ChartAlert, 'anchor'>): ChartAlert => ({
32
+ ...alert,
33
+ anchor: alert.alert.toLowerCase(),
34
+ });
35
+
36
+ export const CHART_ALERTS: readonly ChartAlert[] = [
37
+ rule({
38
+ alert: 'UltimateQueueStalled',
39
+ threshold: 300,
40
+ expr: (s) =>
41
+ `max by (${s.by}, queue) (${s.series('queue_oldest_ready_seconds')}) > ${s.threshold(300)}`,
42
+ for: '5m',
43
+ severity: 'warning',
44
+ summary: 'A queue has a due job nothing has claimed for longer than the threshold',
45
+ }),
46
+ rule({
47
+ alert: 'UltimateDeadLetters',
48
+ expr: (s) => `max by (${s.by}, queue) (${s.series('queue_dead_jobs')}) > 0`,
49
+ for: '5m',
50
+ severity: 'warning',
51
+ summary: 'A queue holds dead-lettered jobs',
52
+ }),
53
+ rule({
54
+ alert: 'UltimateLeaseLost',
55
+ expr: (s) => `sum by (${s.by}, queue) (rate(${s.series('job_leases_lost_total')}[5m])) > 0`,
56
+ for: '5m',
57
+ severity: 'warning',
58
+ summary: 'Job leases are lapsing while their jobs still run, so jobs run twice',
59
+ }),
60
+ rule({
61
+ alert: 'UltimateFramesDropped',
62
+ expr: (s) => `sum by (${s.by}) (rate(${s.series('channel_frames_dropped_total')}[5m])) > 0`,
63
+ for: '5m',
64
+ severity: 'warning',
65
+ summary: 'Sync nodes are dropping channel frames to socket backpressure',
66
+ }),
67
+ rule({
68
+ alert: 'UltimateServerErrors',
69
+ threshold: 0.05,
70
+ expr: (s) =>
71
+ `sum by (${s.by}) (rate(${s.series('http_requests_total', 'status="5xx"')}[5m])) / ` +
72
+ `sum by (${s.by}) (rate(${s.series('http_requests_total')}[5m])) > ${s.threshold(0.05)}`,
73
+ for: '10m',
74
+ severity: 'critical',
75
+ summary: 'The share of requests answered 5xx is above the threshold',
76
+ }),
77
+ rule({
78
+ alert: 'UltimateTargetDown',
79
+ expr: (s) => `count by (${s.by}, ${s.component}) (${s.series('up')} == 0) > 0`,
80
+ for: '5m',
81
+ severity: 'critical',
82
+ summary: 'A role has a scrape target that is down',
83
+ }),
84
+ ];
85
+
86
+ /** Where `runbook_url` points unless `alerts.runbookBase` says otherwise. */
87
+ export const RUNBOOK_BASE =
88
+ 'https://github.com/developerz-ai/ultimate/blob/main/docs/ops/06-runbooks.md';
89
+
90
+ const matchers = (own: string, extra?: string): string =>
91
+ [own, extra].filter((part) => part !== undefined && part !== '').join(',');
92
+
93
+ /**
94
+ * The chart's scope: one release in one namespace, the two labels the ServiceMonitor's
95
+ * `targetLabels` and the operator put on every series. A threshold reads the rule's value.
96
+ */
97
+ export const K8S_SCOPE: AlertScope = {
98
+ series: (name, extra) =>
99
+ `${name}{${matchers('namespace="{{ $.Release.Namespace }}",app_kubernetes_io_instance="{{ $.Release.Name }}"', extra)}}`,
100
+ by: 'namespace, app_kubernetes_io_instance',
101
+ component: 'app_kubernetes_io_component',
102
+ threshold: (fallback) => `{{ ternary $r.threshold ${fallback} (hasKey $r "threshold") }}`,
103
+ };
104
+
105
+ const plainScope = (own: string): AlertScope => ({
106
+ series: (name, extra) => {
107
+ const inner = matchers(own, extra);
108
+ return inner === '' ? name : `${name}{${inner}}`;
109
+ },
110
+ by: 'job',
111
+ component: 'instance',
112
+ threshold: (fallback) => String(fallback),
113
+ });
114
+
115
+ /** The rules as one plain Prometheus rule group, defaults in place, scoped by `own` matchers. */
116
+ export function alertRuleGroups(own = ''): string {
117
+ const scope = plainScope(own);
118
+ const rules = CHART_ALERTS.map(
119
+ (a) => ` - alert: ${a.alert}
120
+ expr: >-
121
+ ${a.expr(scope)}
122
+ for: ${a.for}
123
+ labels:
124
+ severity: ${a.severity}
125
+ annotations:
126
+ summary: ${JSON.stringify(a.summary)}
127
+ runbook_url: ${JSON.stringify(`${RUNBOOK_BASE}#${a.anchor}`)}
128
+ `,
129
+ ).join('');
130
+ return `groups:\n - name: ultimate\n rules:\n${rules}`;
131
+ }
132
+
133
+ /** One rule of the chart template: every field a value under `alerts.rules.<alert>` may replace. */
134
+ const chartRule = (
135
+ a: ChartAlert,
136
+ ): string => ` {{- $r := index $rules "${a.alert}" | default dict }}
137
+ {{- if ne (toString $r.enabled) "false" }}
138
+ - alert: ${a.alert}
139
+ expr: >-
140
+ ${a.expr(K8S_SCOPE)}
141
+ for: {{ $r.for | default "${a.for}" }}
142
+ labels:
143
+ severity: {{ $r.severity | default "${a.severity}" }}
144
+ annotations:
145
+ summary: ${JSON.stringify(a.summary)}
146
+ runbook_url: {{ printf "%s#${a.anchor}" $base | quote }}
147
+ {{- end }}
148
+ `;
149
+
150
+ /**
151
+ * The `enabled` switch both monitoring objects share: `auto` (and a values file with no such block)
152
+ * renders when the cluster serves the operator's kind, `true` forces it, `false` declines it.
153
+ */
154
+ const operatorGate = (
155
+ key: string,
156
+ kind: string,
157
+ ): string => `{{- $cfg := .Values.${key} | default dict }}
158
+ {{- $mode := toString (ternary $cfg.enabled "auto" (hasKey $cfg "enabled")) }}
159
+ {{- if not (has $mode (list "auto" "true" "false")) }}
160
+ {{- fail (printf "${key}.enabled is %q — set it to auto (render when the cluster serves monitoring.coreos.com/v1/${kind}), true or false" $mode) }}
161
+ {{- end }}
162
+ {{- if or (eq $mode "true") (and (eq $mode "auto") (.Capabilities.APIVersions.Has "monitoring.coreos.com/v1/${kind}")) }}`;
163
+
164
+ const alertRulesTemplate = (chart: string): string => `{{/*
165
+ The alert rules for this release's own series, written from @ultimat3/cli's rule table. Each rule
166
+ reads alerts.rules.<alert> for threshold, for, severity and enabled; its runbook_url is
167
+ alerts.runbookBase plus the rule's anchor. Rendered by capability, like servicemonitor.yaml.
168
+ */}}
169
+ ${operatorGate('alerts', 'PrometheusRule')}
170
+ {{- $rules := $cfg.rules | default dict }}
171
+ {{- $base := $cfg.runbookBase | default "${RUNBOOK_BASE}" }}
172
+ apiVersion: monitoring.coreos.com/v1
173
+ kind: PrometheusRule
174
+ metadata:
175
+ name: {{ include "${chart}.fullname" . }}
176
+ labels:
177
+ {{- include "${chart}.labels" . | nindent 4 }}
178
+ {{- with $cfg.labels }}
179
+ {{- toYaml . | nindent 4 }}
180
+ {{- end }}
181
+ spec:
182
+ groups:
183
+ - name: {{ include "${chart}.fullname" . }}.ultimate
184
+ rules:
185
+ ${CHART_ALERTS.map(chartRule).join('')}{{- end }}
186
+ `;
187
+
188
+ const serviceMonitorTemplate = (chart: string): string => `{{/*
189
+ Prometheus-operator discovery for the scrape listener every serving role opens, rendered by
190
+ capability: serviceMonitor.enabled auto (the default, and a values file with no such block) renders
191
+ it when the cluster serves the kind, so a cluster without the operator still installs. ONE object
192
+ for the release: every role's Service publishes the same \`metrics\` port name. targetLabels carry
193
+ the role and the release onto every series, which is what alert-rules.yaml and an HPA key on.
194
+ */}}
195
+ ${operatorGate('serviceMonitor', 'ServiceMonitor')}
196
+ apiVersion: monitoring.coreos.com/v1
197
+ kind: ServiceMonitor
198
+ metadata:
199
+ name: {{ include "${chart}.fullname" . }}
200
+ labels:
201
+ {{- include "${chart}.labels" . | nindent 4 }}
202
+ {{- with $cfg.labels }}
203
+ {{- toYaml . | nindent 4 }}
204
+ {{- end }}
205
+ spec:
206
+ selector:
207
+ matchLabels:
208
+ app.kubernetes.io/name: {{ include "${chart}.name" . }}
209
+ app.kubernetes.io/instance: {{ .Release.Name }}
210
+ targetLabels:
211
+ - app.kubernetes.io/component
212
+ - app.kubernetes.io/instance
213
+ endpoints:
214
+ # The Service port NAME, never the integer: moving metricsPort moves nothing here.
215
+ - port: metrics
216
+ path: /metrics
217
+ interval: {{ $cfg.interval | default "15s" }}
218
+ scrapeTimeout: {{ $cfg.scrapeTimeout | default "10s" }}
219
+ {{- end }}
220
+ `;
221
+
222
+ /**
223
+ * The chart's two monitoring templates. The framework chart's copies (\`docker/helm/templates/\`)
224
+ * are these bytes for the chart named \`ultimate\`, held by \`scaffold-helm-parity.test.ts\`.
225
+ */
226
+ export function helmMonitoringFiles(app: NameSet): readonly GeneratedFile[] {
227
+ return [
228
+ {
229
+ path: 'docker/helm/templates/servicemonitor.yaml',
230
+ contents: serviceMonitorTemplate(app.kebab),
231
+ },
232
+ { path: 'docker/helm/templates/alert-rules.yaml', contents: alertRulesTemplate(app.kebab) },
233
+ ];
234
+ }
@@ -3,6 +3,7 @@
3
3
  // Split from scaffold-helm.ts, which holds the chart's INPUTS (Chart.yaml, values.yaml).
4
4
 
5
5
  import type { GeneratedFile, NameSet } from './naming';
6
+ import { helmMonitoringFiles } from './scaffold-helm-alerts';
6
7
  import { helmFenceFiles } from './scaffold-helm-fences';
7
8
 
8
9
  const helpers = (
@@ -169,7 +170,7 @@ nobody bound. Derived here rather than stated twice in values.yaml, where the tw
169
170
  periodSeconds: 15
170
171
  failureThreshold: 4
171
172
  {{- /*
172
- The worker's retire, opt-in: roles.worker.retireSeconds > 0. The preStop sends PID 1 SIGUSR2 —
173
+ The worker's retire, on by default: roles.worker.retireSeconds > 0. The preStop sends PID 1 SIGUSR2 —
173
174
  the framework's retire: stop claiming, finish every held job however long it takes, abort
174
175
  nothing, then drain and exit 0 — and waits until PID 1 is gone, so the SIGTERM that follows finds
175
176
  nothing left to cut off. It re-sends the signal every 5 s while it waits: PID 1 has no default
@@ -185,7 +186,7 @@ nobody bound. Derived here rather than stated twice in values.yaml, where the tw
185
186
  command:
186
187
  - bun
187
188
  - -e
188
- - "process.kill(1, 'SIGUSR2'); for (let n = 1; ; n++) { try { process.kill(1, n % 5 === 0 ? 'SIGUSR2' : 0); } catch { break; } await Bun.sleep(1000); }"
189
+ - "process.kill(1, 'SIGUSR2'); const end = Date.now() + {{ int $cfg.retireSeconds }} * 1000; for (let n = 1; Date.now() < end; n++) { try { process.kill(1, n % 5 === 0 ? 'SIGUSR2' : 0); } catch { break; } await Bun.sleep(1000); }"
189
190
  {{- end }}
190
191
  {{- end }}
191
192
  resources: {{- toYaml $cfg.resources | nindent 4 }}
@@ -196,7 +197,7 @@ nobody bound. Derived here rather than stated twice in values.yaml, where the tw
196
197
 
197
198
  {{/*
198
199
  A role's terminationGracePeriodSeconds, in the order the kubelet's budget is spent: the preStop
199
- sleep (web and sync, 1.30+), the worker's opt-in retire (roles.worker.retireSeconds), the
200
+ sleep (web and sync, 1.30+), the worker's retire (roles.worker.retireSeconds), the
200
201
  readiness grace (web and sync — every other role drains with none), the drain budget (app.config.ts drain.deadlineMs, every role;
201
202
  the worker's is drain.workerDeadlineSeconds instead when > 0, from drain.workerDeadlineMs) and the teardown margin.
202
203
  Defaults: 45s web/sync (40s below 1.30), 35s the rest. x deploy --method helm sets
@@ -408,7 +409,7 @@ holds at minReplicas, which is why these are opt-in rather than a default nobody
408
409
 
409
410
  autoscaling.type says whose number it is. Pods (the default) is a per-pod series — rps, open
410
411
  sockets — averaged across pods. External is ONE series for the whole role, divided by the replica
411
- count: queue_depth is that shape, because every worker publishes the same global backlog, and read
412
+ count: queue_demand is that shape, because every serving worker publishes the same number, and read
412
413
  as Pods it asked for N times the workers the queue needed. Any other type fails the render.
413
414
  */}}
414
415
  {{- range $role, $cfg := .Values.roles }}
@@ -430,7 +431,7 @@ spec:
430
431
  maxReplicas: {{ $cfg.autoscaling.maxReplicas }}
431
432
  {{- $type := default "Pods" $cfg.autoscaling.type }}
432
433
  {{- if not (has $type (list "Pods" "External")) }}
433
- {{- fail (printf "roles.%s.autoscaling.type is %q — set it to Pods (a per-pod series) or External (one series for the whole role, e.g. queue_depth)" $role $type) }}
434
+ {{- fail (printf "roles.%s.autoscaling.type is %q — set it to Pods (a per-pod series) or External (one series for the whole role, e.g. queue_demand)" $role $type) }}
434
435
  {{- end }}
435
436
  metrics:
436
437
  {{- if eq $type "External" }}
@@ -469,5 +470,6 @@ export function helmTemplateFiles(app: NameSet): readonly GeneratedFile[] {
469
470
  { path: 'docker/helm/templates/ingress.yaml', contents: ingress(app) },
470
471
  { path: 'docker/helm/templates/hpa.yaml', contents: hpa(app) },
471
472
  ...helmFenceFiles(app),
473
+ ...helmMonitoringFiles(app),
472
474
  ];
473
475
  }
@@ -6,6 +6,7 @@
6
6
  // scaffold-helm-templates.ts.
7
7
 
8
8
  import type { GeneratedFile, NameSet } from './naming';
9
+ import { RUNBOOK_BASE } from './scaffold-helm-alerts';
9
10
  import { helmTemplateFiles } from './scaffold-helm-templates';
10
11
 
11
12
  /**
@@ -17,6 +18,37 @@ import { helmTemplateFiles } from './scaffold-helm-templates';
17
18
  */
18
19
  export const RUNTIME_UID = 1000;
19
20
 
21
+ /**
22
+ * The two monitoring blocks of `values.yaml`, each a top-level key with its comment. Exported
23
+ * because `x g chart-alerts` appends whichever one an existing app's `values.yaml` lacks: one
24
+ * copy of the bytes, here, for the scaffold and the backfill.
25
+ */
26
+ export const SERVICE_MONITOR_VALUES = `# Prometheus-operator discovery for that port (templates/servicemonitor.yaml) and the alert rules on
27
+ # the series it scrapes (templates/alert-rules.yaml). \`enabled\` on each:
28
+ # auto render when the cluster serves the operator's kind, so a cluster without it still
29
+ # installs. \`helm template\` answers from \`--api-versions\`: an offline render (GitOps) gets
30
+ # neither unless it passes --api-versions monitoring.coreos.com/v1/ServiceMonitor and
31
+ # --api-versions monitoring.coreos.com/v1/PrometheusRule
32
+ # true always render; \`helm install\` fails where the CRD is missing
33
+ # false never render
34
+ serviceMonitor:
35
+ enabled: auto
36
+ interval: 15s
37
+ scrapeTimeout: 10s
38
+ labels: {} # what your operator's serviceMonitorSelector matches, if it uses one
39
+
40
+ `;
41
+
42
+ export const ALERTS_VALUES = `# Each rule's runbook_url is runbookBase#<alert, lower-cased>. Per rule, under rules.<alert>:
43
+ # threshold, for, severity, enabled — e.g. rules: { UltimateQueueStalled: { threshold: 600 } }.
44
+ alerts:
45
+ enabled: auto
46
+ runbookBase: ${RUNBOOK_BASE}
47
+ labels: {} # what your operator's ruleSelector matches, if it uses one
48
+ rules: {}
49
+
50
+ `;
51
+
20
52
  /**
21
53
  * `appVersion` is the default image tag (`image.tag: ""` below), so it must name a tag that can
22
54
  * exist. `x deploy --image <ref>` overrides both keys, which is the path that actually ships.
@@ -80,7 +112,7 @@ networkPolicy:
80
112
  # METRICS_PORT in the container; move one and the other follows.
81
113
  metricsPort: 9090
82
114
 
83
- # The image's \`bun\` user, the same uid as the Dockerfile's USER: move one and the other follows.
115
+ ${SERVICE_MONITOR_VALUES}${ALERTS_VALUES}# The image's \`bun\` user, the same uid as the Dockerfile's USER: move one and the other follows.
84
116
  podSecurityContext:
85
117
  runAsNonRoot: true
86
118
  runAsUser: ${RUNTIME_UID}
@@ -167,11 +199,12 @@ roles:
167
199
  worker:
168
200
  enabled: true
169
201
  replicas: 2
170
- # The retire, OFF at 0. Set to the longest a held job may run, in seconds, and the worker's
171
- # preStop sends its PID 1 SIGUSR2: stop claiming, finish every held job, abort nothing, exit 0
172
- # — for a job whose side effect must happen at most once and may outlast its drain budget.
173
- # Added to this role's terminationGracePeriodSeconds, which the kubelet counts the preStop against.
174
- retireSeconds: 0
202
+ # The retire, ON: a deploy or a scale-down never cuts a job it could wait out. app.config.ts
203
+ # drain.workerRetireMs / 1000, set by x deploy, and 0 turns it off. The worker's preStop sends
204
+ # its PID 1 SIGUSR2: stop claiming, finish every held job, abort nothing, exit 0. An idle worker
205
+ # exits at once, and a Terminating pod does not hold a rollout back. Added to this role's
206
+ # terminationGracePeriodSeconds, which the kubelet counts the preStop against.
207
+ retireSeconds: 3600
175
208
  resources:
176
209
  requests: { cpu: 200m, memory: 256Mi }
177
210
  limits: { memory: 1Gi }
@@ -179,11 +212,12 @@ roles:
179
212
  enabled: false
180
213
  minReplicas: 1
181
214
  maxReplicas: 50
182
- # External: every worker publishes the SAME global backlog, so averaged as a Pods metric it
215
+ # External: every serving worker publishes the SAME number, so averaged as a Pods metric it
183
216
  # asked for N times the workers the queue needed. Expose it from your adapter deduplicated —
184
- # max(queue_depth), never sum, which would multiply it by the pod count again.
217
+ # max by (queue) (queue_demand), never sum, which would multiply it by the pod count again.
185
218
  type: External
186
- metric: queue_depth # jobs waiting, exported by the worker role
219
+ metric: queue_demand # jobs held by serving workers plus jobs due, per queue
220
+ # Set it to one pod's jobs.maxInFlight; x deploy --method helm sets it when app.config.ts declares it.
187
221
  targetAverageValue: "100"
188
222
 
189
223
  scheduler:
@@ -213,9 +247,9 @@ ingress:
213
247
  `;
214
248
 
215
249
  /**
216
- * The chart for a new app: six kinds of object, all of them core Kubernetes. A ServiceMonitor and
217
- * a PodDisruptionBudget are deliberately absent — the first needs a CRD `helm install` fails on
218
- * where no Prometheus operator is installed, and both are cluster policy rather than app topology.
250
+ * The chart for a new app: core Kubernetes objects, plus a ServiceMonitor and a PrometheusRule that
251
+ * render only where the cluster serves those kinds. A PodDisruptionBudget is deliberately absent:
252
+ * it is cluster policy rather than app topology.
219
253
  */
220
254
  export function helmFiles(app: NameSet): readonly GeneratedFile[] {
221
255
  return [
@@ -143,8 +143,12 @@ const i18nCatalog = (app: NameSet): string =>
143
143
  'app.dashboard.statToday': 'Created today',
144
144
  'app.dashboard.hintToday': 'so far, UTC',
145
145
  'app.dashboard.chartTitle': 'Posts per day',
146
- 'app.dashboard.chartRange': 'Last 14 days, UTC',
147
- 'app.dashboard.chartLabel': 'Posts created per day over the last 14 days',
146
+ 'app.dashboard.chartRange': 'Per UTC day, over the window you pick',
147
+ 'app.dashboard.rangeLabel': 'Chart window',
148
+ 'app.dashboard.rangeWeek': 'Last 7 days',
149
+ 'app.dashboard.rangeFortnight': 'Last 14 days',
150
+ 'app.dashboard.rangeMonth': 'Last 30 days',
151
+ 'app.dashboard.chartLabel': 'Posts created per day over the chosen window',
148
152
  'app.dashboard.tableTitle': 'Latest posts',
149
153
  'app.dashboard.tableCaption': 'The ten newest posts',
150
154
  'app.dashboard.columnTitle': 'Title',