@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
package/wiki/Notify.md ADDED
@@ -0,0 +1,208 @@
1
+ # Notify
2
+
3
+ **One declaration, many channels**: fan-out, a preference gate, a digest window, a delivery ledger
4
+ and an in-app inbox. Package `@ultimat3/notify` (tier 4) — the full reference is
5
+ [`packages/notify/README.md`](https://github.com/developerz-ai/ultimate/blob/main/packages/notify/README.md).
6
+
7
+ `notifier()` is a **job factory**: it returns a `JobHandle`, so a notification inherits retry, the
8
+ dead-letter path, cancellation, `x jobs show` and its manifest row — the same shape `backfill()`
9
+ and `llm()` take ([The eight primitives](The-Eight-Primitives)). It is not a ninth primitive.
10
+
11
+ ```ts
12
+ import { inAppChannel, mailChannel, notifier, t } from '@ultimat3/notify';
13
+
14
+ export const commentPosted = notifier({
15
+ name: 'post.commented',
16
+ input: t.object({ postId: t.uuid, orgId: t.uuid, author: t.string }),
17
+ tenant: (params) => params.orgId, // required, as on job()
18
+ key: (params) => `comment:${params.postId}`, // the queue's dedupe key AND the ledger's event key
19
+ recipients: ({ input, ctx }) => posts.subscribers(input.postId, ctx.signal),
20
+ deliver: [
21
+ { channel: inAppChannel() }, // immediately
22
+ {
23
+ channel: mailChannel({ mailer }),
24
+ wait: '10m', // then re-read the condition: a mute in minute 3 wins
25
+ unless: ({ event }) => event.params.author === 'system',
26
+ digest: { window: '1h', group: (event) => event.params.postId },
27
+ },
28
+ ],
29
+ });
30
+
31
+ await commentPosted.enqueue({ params: { postId, orgId, author } });
32
+ ```
33
+
34
+ ## The stores
35
+
36
+ `setNotifyStores({ ledger, inbox, digest, preferences })` once at boot, whole-object replacement:
37
+
38
+ | Store | Default | Postgres |
39
+ |---|---|---|
40
+ | `ledger` — the delivery claim | in-memory (one process is genuinely deduped) | `postgresDeliveryLedger({ executor, windowMs })` — `windowMs` never shorter than your idempotency window |
41
+ | `preferences` — the gate | allow all | yours: **the gate ships, what it reads never does** — your taxonomy, your quiet hours |
42
+ | `inbox` | none — `X_NOTIFY_STORE_MISSING` | `postgresInboxStore({ executor })` |
43
+ | `digest` | none — `X_NOTIFY_STORE_MISSING` | `postgresDigestStore({ executor })` — one window per slot across replicas; `memoryDigestStore()` for one process |
44
+
45
+ The hourly `x.purge` job sweeps the Postgres ledger, inbox and digest windows (a closed window a week old, by default) — and, in the same pass, `x_job_events`, the stored bus `step.waitForEvent` reads. The inbox is swept only when your
46
+ `app.config.ts` sets `notify.inboxReadRetentionMs` / `notify.inboxUnreadRetentionMs` — when an unread
47
+ message disappears is your decision ([Configuration](Configuration)).
48
+
49
+ ## Channels, the inbox, and at-least-once
50
+
51
+ - `deliveryChannel(name, fn)` delivers per recipient; `bulkChannel(name, fn)` makes **one** call for the
52
+ whole audience (a Slack post, a webhook) and cannot take a digest (`X_NOTIFY_DIGEST_UNSUPPORTED`).
53
+ `inAppChannel()`, `mailChannel({ mailer })` and `pushChannel({ pusher, message })` ship; `mailer` and
54
+ `pusher` are structural, so [Mail](Mail) and `@ultimat3/pwa` plug in without an import — [Push](#push).
55
+ - `requireInbox(name)` answers `list`, `unreadCount`, `markSeen`, `markRead`. `seenAt` and `readAt`
56
+ are two facts, and the unread count is derived, never stored.
57
+ - A recipient id named twice in the audience is one recipient: the list is deduplicated by `id`,
58
+ first entry kept, before any channel runs.
59
+ - A replay does not send twice: the step checkpoint per channel and recipient, and the ledger's
60
+ atomic claim on `(notifier, key, channel, recipient)` taken before the send.
61
+ - Two attempts at once: one sends. The other raises `X_NOTIFY_DELIVERY_HELD` and retries past the
62
+ holder's `reclaimAfterMs` (5 minutes by default), so a crashed attempt's recipient is still sent to
63
+ and a late `settle` moves nothing.
64
+ - Entries fire in `wait` order; `if` / `unless` run **after** the wait, on the attempt that delivers.
65
+
66
+ Codes: `X_NOTIFY_CHANNELS_EMPTY`, `X_NOTIFY_CHANNEL_DUPLICATE`, `X_NOTIFY_DIGEST_UNSUPPORTED`,
67
+ `X_NOTIFY_FANOUT_TOO_WIDE` (default cap 500 recipients), `X_NOTIFY_STORE_MISSING`,
68
+ `X_NOTIFY_DELIVERY_FAILED`, and native push's `X_NOTIFY_PUSH_DEVICE_GONE`,
69
+ `X_NOTIFY_PUSH_PROVIDER_REFUSED`, `X_NOTIFY_PUSH_MESSAGE_INVALID` — [Error codes](Error-Codes).
70
+
71
+ ## Push
72
+
73
+ `pushChannel` delivers one notification to **every device the recipient subscribed**, each in the
74
+ locale that browser subscribed with. `As of 26.1.0`. The transport is `@ultimat3/pwa`'s `webPush()`:
75
+ RFC 8291 encryption and an RFC 8292 (VAPID, ES256) token, on WebCrypto — no dependency.
76
+
77
+ ```ts
78
+ import type { Recipient } from '@ultimat3/notify';
79
+ import { notifier, pushChannel, t } from '@ultimat3/notify';
80
+ import { webPush } from '@ultimat3/pwa';
81
+
82
+ // The app's own read: who follows a post. A notifier never touches the database itself.
83
+ declare const posts: {
84
+ subscribers(postId: string, signal: AbortSignal): Promise<readonly Recipient[]>;
85
+ };
86
+
87
+ export const commentPushed = notifier({
88
+ name: 'post.commented.push',
89
+ input: t.object({ postId: t.uuid, commenter: t.string }),
90
+ tenant: 'none',
91
+ key: (params) => `comment-push:${params.postId}`,
92
+ // The audience — every device of each of them is reached. Omitted, the audience is empty and
93
+ // nothing is sent unless every enqueue names its own `recipients`.
94
+ recipients: ({ input, ctx }) => posts.subscribers(input.postId, ctx.signal),
95
+ deliver: [
96
+ {
97
+ channel: pushChannel<{ postId: string; commenter: string }>({
98
+ pusher: webPush(),
99
+ // Catalog KEYS, never text: rendered per subscription, in its own locale.
100
+ message: ({ event }) => ({
101
+ titleKey: 'push.comment.title',
102
+ bodyKey: 'push.comment.body',
103
+ params: { who: event.params.commenter },
104
+ url: `/posts/${event.params.postId}`, // a path on this app; anything else opens the root
105
+ tag: `post:${event.params.postId}`, // set it: a retry re-sends, a tag makes it a replacement
106
+ }),
107
+ }),
108
+ },
109
+ ],
110
+ });
111
+ ```
112
+
113
+ | Piece | Where |
114
+ |---|---|
115
+ | config | `pwa: { push: true, vapid: { subject: 'mailto:ops@example.com' } }` ([Configuration](Configuration)) |
116
+ | key pair | `x vapid create` seals `ULTIMATE_VAPID_PUBLIC_KEY` + `ULTIMATE_VAPID_PRIVATE_KEY`; a local process with neither signs with a published development pair, a deployed boot refuses it (`X_PWA_VAPID_KEY_MISSING`) |
117
+ | subscriptions | `x_push_subscriptions`, a framework table the boot applies; the boot installs the store |
118
+ | subscribe / unsubscribe | `export const subscribePush = pushSubscribe({ permission: 'push:subscribe' })` and `pushUnsubscribe(…)` in an actions module — real actions, on the one authz path, stored against `ctx.actor.id`; an agent never subscribes |
119
+ | the button | `subscribeToPush({ save: client.subscribePush })` from `@ultimat3/pwa/client`, on a click: asks permission, subscribes with the key in `<meta name="x-push-key">`, saves the page's locale and the device's zone |
120
+
121
+ What each push-service answer does:
122
+
123
+ | Answer | Outcome |
124
+ |---|---|
125
+ | 201 | delivered |
126
+ | 404 / 410, or an expired subscription | the subscription is deleted; nothing is retried |
127
+ | 429 / 5xx / no connection | `X_PWA_PUSH_FAILED` — thrown after every other device was tried, so the delivery fails as `X_NOTIFY_DELIVERY_FAILED` and the notifier job retries on its own backoff |
128
+ | 400 / 413 | `X_PWA_PUSH_REJECTED` — logged and counted, never retried |
129
+ | 401 / 403 | `X_PWA_PUSH_REJECTED`, and the subscription is deleted: it was made with a VAPID key this server no longer signs with. `subscribeToPush` replaces such a browser's subscription the next time it asks |
130
+
131
+ A retry re-sends to the devices that already got it; the `tag` makes that a replacement on the device
132
+ rather than a second notification. `renotify` without a `tag` is dropped with a warning — the spec
133
+ makes that pair show nothing at all.
134
+
135
+
136
+ ## Native push — iOS and Android
137
+
138
+ `nativePush({ devices, apns, fcm })` is the pusher for native apps, `As of 2026-11-06`: APNs over
139
+ HTTP/2 with token auth (an ES256 JWT over the team's `.p8` key) and FCM HTTP v1 (a service-account
140
+ assertion exchanged for an OAuth token) — WebCrypto and core's `outbound()` with a deadline, no SDK.
141
+ Both credentials are cached and re-minted before they expire.
142
+
143
+ ```ts
144
+ import {
145
+ memoryPushDeviceStore,
146
+ nativePush,
147
+ notifier,
148
+ pushChannel,
149
+ pushRegister,
150
+ pushUnregister,
151
+ t,
152
+ } from '@ultimat3/notify';
153
+
154
+ // Production: `postgresPushDeviceStore({ executor: dbExecutor() })` — `x_push_devices`, a
155
+ // framework table the boot applies. ONE store, handed to the actions and the pusher alike.
156
+ const devices = memoryPushDeviceStore();
157
+
158
+ export const registerDevice = pushRegister({
159
+ permission: 'push:subscribe',
160
+ platforms: ['ios', 'android'],
161
+ devices,
162
+ });
163
+ export const unregisterDevice = pushUnregister({
164
+ permission: 'push:subscribe',
165
+ platforms: ['ios', 'android'],
166
+ devices,
167
+ });
168
+
169
+ const phones = nativePush({
170
+ devices,
171
+ apns: { teamId: 'TEAM123456', keyId: 'KEY1234567', topic: 'com.example.app' },
172
+ fcm: { projectId: 'example-app', clientEmail: 'push@example-app.iam.gserviceaccount.com' },
173
+ });
174
+
175
+ export const approvalRequested = notifier({
176
+ name: 'approval.requested',
177
+ input: t.object({ approvalId: t.uuid }),
178
+ tenant: 'none',
179
+ key: (params) => `approval:${params.approvalId}`,
180
+ deliver: [
181
+ {
182
+ // Data-only: the id and nothing else. Nothing reaches a lock screen or the provider.
183
+ channel: pushChannel<{ approvalId: string }>({
184
+ pusher: phones,
185
+ data: ({ event }) => ({ approvalId: event.params.approvalId }),
186
+ }),
187
+ },
188
+ ],
189
+ });
190
+ ```
191
+
192
+ | Piece | Where |
193
+ |---|---|
194
+ | private keys | `x secrets set ULTIMATE_APNS_PRIVATE_KEY` (the `.p8`) · `x secrets set ULTIMATE_FCM_PRIVATE_KEY` (the service account's `private_key`). Never an option; read when a token is minted, boxed in a `Secret`, never logged. Missing: `X_ENV_MISSING`; unreadable: `X_CONFIG_INVALID` |
195
+ | devices | `x_push_devices`, keyed `(platform, token)` — a phone a second person signs in on is theirs now |
196
+ | register | `registerDevice` on every launch; `previousToken` when the OS rotated it. `unregisterDevice` on sign-out. Stored against `ctx.actor.id`; an agent never registers |
197
+ | data-only | `pushChannel({ pusher, data })` — APNs `background`, priority 5, `content-available: 1`; FCM with no `notification`. ≤ 8 string keys, ≤ 256 characters, no reserved key: else `X_NOTIFY_PUSH_MESSAGE_INVALID`. `webPush()` cannot take it — a type error |
198
+ | visible | `pushChannel({ pusher, message })`, the same `NotifyPushMessage` as the web: rendered per device locale |
199
+
200
+ | Provider answer | Outcome |
201
+ |---|---|
202
+ | 2xx | delivered |
203
+ | APNs 410 · FCM `UNREGISTERED` | `X_NOTIFY_PUSH_DEVICE_GONE` — the device row is deleted, the delivery settles |
204
+ | other 4xx | `X_NOTIFY_PUSH_PROVIDER_REFUSED` — logged, next device tried; fails the delivery when no device took it. 401/403 also drops the cached credential |
205
+ | 429 · 5xx · timeout | rethrown after every other device was tried: `X_NOTIFY_DELIVERY_FAILED`, the job retries |
206
+
207
+ A bare FCM 404 is never "gone" (a wrong `projectId` answers it too), nor is APNs `BadDeviceToken`
208
+ (a sandbox token sent to production): reading either as dead would delete every device.
@@ -0,0 +1,245 @@
1
+ # Observability
2
+
3
+ Metrics, spans, and structured logs — all in `@ultimat3/core`, all dependency-free.
4
+
5
+ `As of 2026-08`. Stable API — semver from here ([Upgrading](Upgrading)).
6
+
7
+ Counter, gauge and histogram sit on the OpenTelemetry data model, aggregate in process, and are read through one `collectMetrics()`. The wire format is a **driver**: `MetricExporter` is the seam, the default is a no-op, and `metricsText()` renders the Prometheus/OpenMetrics scrape body. An **OTLP client ships** `As of 2026-08`: `otlpMetricExporter()` and `otlpSpanExporter()` speak OTLP/HTTP JSON over `fetch` and read `OTEL_EXPORTER_OTLP_ENDPOINT` (and the `_METRICS_`/`_TRACES_ENDPOINT`, `_HEADERS`, `_PROTOCOL` variants), with no new dependency. Two limits: **gRPC on `:4317` is refused** with `X_OTLP_PROTOCOL_UNSUPPORTED` and will stay refused — HTTP/2 plus protobuf is a dependency and a second wire format, so point the collector's HTTP receiver at `:4318` — and there is **no logs signal**. Scraping `metricsText()` with an agent remains equally supported.
8
+
9
+ ## Instruments
10
+
11
+ ```ts
12
+ import { counter, gauge, histogram } from '@ultimat3/core';
13
+
14
+ const signups = counter('signups_total', { unit: '{signup}', description: 'completed signups' });
15
+ const inflight = gauge('inflight_requests');
16
+ const latency = histogram('external_call_seconds', { unit: 's', bounds: [0.1, 0.5, 1, 5] });
17
+
18
+ signups.add(); // value defaults to 1
19
+ signups.add(3, { plan: 'pro' });
20
+ inflight.record(12);
21
+ inflight.add(-1);
22
+ latency.record(0.42, { host: 'stripe' });
23
+ ```
24
+
25
+ | Kind | Methods | Notes |
26
+ |---|---|---|
27
+ | `counter(name, options?)` | `add(value = 1, attributes = {})` | cumulative and monotonic — a negative delta throws `X_METRIC_VALUE_INVALID` |
28
+ | `gauge(name, options?)` | `record(value, attributes = {})`, `add(delta, attributes = {})` | the kind for a number that can fall. `options.observe` supplies a pull callback |
29
+ | `histogram(name, options?)` | `record(value, attributes = {})` | `options.bounds` overrides `DEFAULT_HISTOGRAM_BOUNDS` |
30
+
31
+ `DEFAULT_HISTOGRAM_BOUNDS` is `0.005 0.01 0.025 0.05 0.075 0.1 0.25 0.5 0.75 1 2.5 5 7.5 10` — seconds, so an HTTP or RPC duration needs no bounds of its own.
32
+
33
+ Defaults when omitted: `unit: '1'`, `description: ''`.
34
+
35
+ | Failure | Code |
36
+ |---|---|
37
+ | a name that is not lowercase `snake_case`, or one name declared as two kinds | `X_METRIC_NAME_INVALID` |
38
+ | `NaN`, `Infinity`, or a counter decremented | `X_METRIC_VALUE_INVALID` |
39
+
40
+ Dotted OTel names survive OTLP but not a Prometheus scrape, and the autoscaler reads the scrape — which is why the name rule is `^[a-z_][a-z0-9_]*$` and not a suggestion.
41
+
42
+ ## Attributes
43
+
44
+ `Readonly<Record<string, string | number | boolean>>`. Every distinct attribute set is its own series, so an attribute carrying an id or a raw path is an unbounded-cardinality bug, not a richer metric.
45
+
46
+ ## Reading them
47
+
48
+ ```ts
49
+ import { collectMetrics, configureMetrics, exportMetrics, startMetricExport } from '@ultimat3/core';
50
+
51
+ collectMetrics(); // a MetricCollection: { at, resource, metrics }
52
+ configureMetrics({ exporter: myExporter }); // install the seam
53
+ exportMetrics(); // collect + hand to the exporter, once
54
+ const stop = startMetricExport(60_000); // periodic; the timer is unref'd. Default 60s
55
+ ```
56
+
57
+ | Type | Shape |
58
+ |---|---|
59
+ | `MetricExporter` | **one** method: `export(collection: MetricCollection): void` |
60
+ | `MetricCollection` | `{ at, resource, metrics }` — `at` is epoch ms from the configured clock, metrics sorted by name |
61
+ | `ReadableMetric` | `{ descriptor, points }` |
62
+ | `MetricDescriptor` | `{ name, kind, unit, description }`, `unit` in UCUM |
63
+ | `MetricPoint` | `{ attributes, value }` |
64
+ | `HistogramPoint` | adds `count`, `min`, `max`, `bounds`, `buckets` — `buckets` is one longer than `bounds`, the last being the `+Inf` overflow |
65
+
66
+ Temporality is **cumulative**. Shipped exporters: `noopMetricExporter` (the default) and `memoryMetricExporter()` for tests. `resetMetrics()` restores the defaults and clears the series while keeping declarations — test-only.
67
+
68
+ ## The Prometheus body
69
+
70
+ ```ts
71
+ import { METRICS_CONTENT_TYPE, METRICS_PATH, metricsText } from '@ultimat3/core';
72
+
73
+ METRICS_PATH; // '/metrics'
74
+ METRICS_CONTENT_TYPE; // 'text/plain; version=0.0.4; charset=utf-8'
75
+ metricsText(); // the whole scrape body, from a fresh collectMetrics()
76
+ ```
77
+
78
+ The body always opens with a `target_info` gauge carrying the service identity, then per metric a `# HELP`, a `# TYPE`, and its points. A non-trivial unit is appended to the help text as `<description> (<unit>)`. Histograms render cumulative `_bucket` series including `le="+Inf"`, then `_sum` and `_count`. Labels are sorted alphabetically; `\`, `"` and newline are escaped; `+Inf` / `-Inf` / `NaN` are spelled out.
79
+
80
+ `/metrics` is mounted by **every role**, on its own port — `METRICS_PORT`, default `9090` — not on the role's HTTP port. `startMetricsEndpoint` in `packages/cli/src/metrics-endpoint.ts` starts first and stops last, so `x dev` and production behave identically.
81
+
82
+ | Decision | Why |
83
+ |---|---|
84
+ | A separate port, not beside `/healthz` | the Helm ingress routes `/` `Prefix` to the web Service with no path exclusion, so `/metrics` on 3000 publishes your route patterns, request volumes and error rates to the internet. Nothing in the chart fronts 9090, so it is cluster-internal by construction rather than by an exclusion somebody has to remember |
85
+ | Every role, not just `web` | `worker`, `scheduler` and `replicator` open no HTTP socket at all — a separate listener is the only thing they can ever be scraped on, and `queue_depth` belongs to `worker` |
86
+ | Its own env var, not `PORT + n` | moving the app port must not silently move the port Prometheus is configured against, and the roles that set no `PORT` still need one |
87
+ | `x dev` with no `METRICS_PORT` moves off a taken port | two apps on one laptop both default to 9090: the second binds a free port, logs `ultimate metrics port taken`, and reports the real URL in `x dev --json`'s `metrics`. A declared `METRICS_PORT`, and every container, refuses with `X_PORT_IN_USE` — a scraper was told that port |
88
+ | Answered outside the request pipeline | no auth stage, no rate limit, no locale — a saturated or draining process must still be able to say how saturated it is. Same shape as `/healthz` |
89
+
90
+ ## The series autoscaling reads
91
+
92
+ Declared in `runtime-metrics.ts`, so the role table, the Helm chart and the scrape cannot drift apart.
93
+
94
+ | Instrument | Name | Kind | Unit | Labels |
95
+ |---|---|---|---|---|
96
+ | `requests` | `http_requests_total` | counter | `{request}` | `method`, `route`, `status` — the status **class**, e.g. `2xx` |
97
+ | `requestDuration` | `http_request_duration_seconds` | histogram | `s` | same three |
98
+ | `connections` | `connections` | gauge | `{connection}` | none |
99
+ | `queueDepth` | `queue_depth` | gauge | `{job}` | `queue` |
100
+ | `queueDemand` | `queue_demand` | gauge | `{job}` | `queue` |
101
+ | `jobs` | `jobs_total` | counter | `{job}` | `queue`, `outcome` (`ok \| failed \| dead`) |
102
+ | `leasesLost` | `job_leases_lost_total` | counter | `{job}` | `queue` |
103
+
104
+ `SCALING_METRICS` maps each `ScalingSignal` to its series: `rps` → `http_requests_total`, `ws-connections` → `connections`, `queue-demand` → `queue_demand`; `singleton`, `run-once` and `per-database` map to `null`, because a role pinned at one replica has nothing to scale on. **`rps` is derived from the monotonic counter** — a rate is not a series, so nothing exports one. **`queue_demand` is what the worker must hold**: running on a serving worker, plus due rows on an unpaused queue. `queue_depth` is what waits, and stays for the backlog alert: 10 jobs held read `queue_depth` 0 and `queue_demand` 10, and an HPA on the first shrank the fleet mid-job. Every serving worker publishes the same `queue_demand`, so the adapter rule is `max by (queue) (queue_demand)`, never `sum`; a draining worker publishes 0, since its loop has stopped and a frozen value would win that `max` for its whole retire. `JobDriver.demand()` is required of every driver; Redis keeps no worker registry, so it counts every running row.
105
+
106
+ Recorders: `recordRequest({ method, route, status, durationMs })`, `recordConnection(delta)`, `recordQueueDepth(queue, depth)`, `recordJob(queue, outcome)`, `recordLeaseLost(queue)`.
107
+
108
+ All five are wired `As of 2026-08`, each in the package that owns the event:
109
+
110
+ | Recorder | Called from | Note |
111
+ |---|---|---|
112
+ | `recordRequest` | `packages/http/src/pipeline.ts` — a `finally` around `execute` | counts every request exactly once, including a finalize stage that throws on its own. The label is the route **pattern** (`/posts/:id`), never the concrete path; unmatched paths collapse to one `unmatched` series, so a scanner hitting `/wp-admin` and `/.env` cannot mint series |
113
+ | `recordConnection` | `packages/realtime/src/socket.ts` — `SocketRegistry.add` / `remove` | the only definition of a live connection on a node. Decrements only on a real delete, so a double close cannot drive the gauge negative. Every eviction — a closed socket, the idle sweep, a drain — goes through `sync-node`'s `teardown`, which releases the live subscriptions and the channel topics, **then** calls `remove()`, and **then** starts `presence.leave()`. The leave is last and is deliberately not awaited: the write is on the bus and the close callback is synchronous, so the gauge is already correct while the leave is still in flight. Until 2.0.0 `drain()` called `remove()` directly and released none of the three, so a rolling restart left every drained socket in the shared presence set until TTL — every room rendered each user twice |
114
+ | `recordQueueDepth` | `packages/jobs/src/worker.ts` — top of `tick()` | throttled to 15s, not per poll: `driver.stats()` aggregates the whole jobs table, and a scrape reads it every ~15s anyway. Records `ready` only — a job parked until Tuesday is not backlog. A `stats()` failure is logged and never costs a tick |
115
+ | `recordJob` | `packages/jobs/src/worker.ts` — the outcome branch of a finished job | labelled by queue and outcome only; a label per job name is unbounded in the app's own vocabulary. `suspended` is deliberately unmapped — parking a run with `step.sleep` is control flow, and counting it would make the failure ratio meaningless |
116
+ | `recordLeaseLost` | `packages/jobs/src/heartbeat.ts` — once per job whose lease lapsed | the worker could not renew the visibility window for longer than the window itself, so the queue is free to hand that job to another worker while this one still runs it. One point = one job that ran twice. Alert on any non-zero rate |
117
+
118
+ ### What every process reports about itself
119
+
120
+ Declared in `process-metrics.ts` and `packages/db/src/pool-gauge.ts`, started by the scrape listener — so every role that can be scraped answers them, `worker` and `scheduler` included. `As of 2026-10`.
121
+
122
+ | Name | Kind | Unit | Labels | Reads |
123
+ |---|---|---|---|---|
124
+ | `process_resident_memory_bytes` | gauge | `By` | none | RSS — what the container is charged for |
125
+ | `process_heap_used_bytes` | gauge | `By` | none | JavaScript heap in use, uncollected garbage included; Bun may report it a few percent above `process_heap_total_bytes` |
126
+ | `process_heap_total_bytes` | gauge | `By` | none | heap the engine has reserved |
127
+ | `process_external_memory_bytes` | gauge | `By` | none | buffers, strings and compiled code held outside the heap |
128
+ | `process_cpu_seconds_total` | counter | `s` | none | user + system CPU; the boot's share is counted at start |
129
+ | `process_event_loop_lag_seconds` | histogram | `s` | none | how late a 1,000 ms timer fired, one sample a second |
130
+ | `process_start_time_seconds` | gauge | `s` | none | Unix time the process started |
131
+ | `process_info` | gauge | `1` | `role` | always 1; `role` is `ROLE`, or `x dev`'s roles joined by `+` |
132
+ | `db_pool_max` | gauge | `{connection}` | none | connections this process may open, summed over its pools |
133
+ | `db_pool_in_use` | gauge | `{connection}` | none | connections running a statement or pinned by a transaction |
134
+ | `db_pool_waiting` | gauge | `{statement}` | none | statements and pins queued because the pool is at `max` |
135
+
136
+ - **Cost**: one unref'd timer wake a second — 0.12 millicore measured, on top of the 1.8 an idle Bun process spends — and ~12 µs per reading (`process.memoryUsage()` + `process.cpuUsage()`); a scrape reads the gauges at collection time.
137
+ - **No GC count, no heap object count.** Bun exposes neither cheaply: `bun:jsc`'s `heapStats()` runs a full collection to answer — 9 ms on a process holding only the framework.
138
+ - **The pool series are derived from demand.** `Bun.SQL` publishes no occupancy, so `client.ts` counts every statement from send to settle and every pin from ask to release: demand up to `max` is in use, the rest is waiting. `db_pool_max - db_pool_in_use` is the headroom; there is no idle count.
139
+ - **`queue_wake_live`** (gauge, `worker` only): 1 once a notification has crossed the worker's `LISTEN` session, 0 while jobs start on the poll alone — a transaction-pooling proxy disables the wake without an error.
140
+ - **What grows?** `process_resident_memory_bytes` rising with `process_heap_used_bytes` flat is memory outside the heap (`process_external_memory_bytes`, or the allocator); both rising is a retained structure.
141
+
142
+ ### The one series a `sync` node adds
143
+
144
+ | Instrument | Name | Kind | Unit | Labels |
145
+ |---|---|---|---|---|
146
+ | `channelFramesDropped` | `channel_frames_dropped_total` | counter | `{frame}` | **none** |
147
+
148
+ Declared in `packages/realtime/src/socket.ts`, not in `runtime-metrics.ts`: that file is the series every Ultimate process emits and the chart scales on, and this one exists only where channels do.
149
+
150
+ **No labels, on purpose.** A topic is client-chosen — `topic()` admits any `[A-Za-z0-9_-]+` segment — so a per-topic label is unbounded series one socket can mint. The topic goes in the log line instead: `channel.frames_dropped` at `warn`, with `{ topic, dropped, total }`.
151
+
152
+ **It is a data-loss counter, not a saturation one.** Nothing replays a channel frame: a topic has no cursor and no re-snapshot, so a frame backpressure refused is gone → [Realtime](Realtime). Alert on any non-zero rate. The live-query path is the one that repairs, and its drops are invisible here by design. `SocketRegistry.droppedChannelFrames` is the same number in process, for a test or a benchmark that cannot scrape.
153
+
154
+ CPU autoscaling is wrong for `sync` and `worker`: a node holding 80k idle sockets is near-zero CPU and near-capacity, and a worker blocked on a slow HTTP call is idle CPU with a growing backlog. That is why the two custom series exist → [Deployment](Deployment).
155
+
156
+ ## The database, on demand
157
+
158
+ `x db diagnostics --json` (`As of 2026-11`) answers what the series cannot: who blocks whom (`data.blockers.rows[].blockedBy`), what each session is doing, the slowest statements (`pg_stat_statements`, when an operator installed it), unused and invalid indexes, `NOT VALID` constraints, dead tuples and the last autovacuum. Facts only, exit 0; `/readyz` stays the one-`select 1` probe. Read-only, 2 s per statement, no user table read, literals printed as `?`; whoever runs it sees the redacted SQL of every session on the app's database role → [CLI reference](CLI-Reference#x-db). The same report is `dbDiagnostics()` from `@ultimat3/db`.
159
+
160
+ ## Spans and logs
161
+
162
+ | Concern | Where |
163
+ |---|---|
164
+ | Tracing | always on, not a flag. `otel.endpoint` absent means spans are still recorded and exported nowhere → [Configuration](Configuration) |
165
+ | Head sampling | `otel.sampling`, default `0.1`; errors are always sampled |
166
+ | OTLP delivery | a 429, 502, 503, 504 or a transport failure is retried, 3 attempts with full jitter and `Retry-After` honoured, inside the exporter's `timeoutMs` (default 10 s, the whole export); any other status is one POST. One `otlp export rejected`/`failed` line per batch, with `attempts`. A collector that accepted and then answered 503 receives that batch twice; span ids dedupe it |
167
+ | Error grouping | `errorFingerprint(report)`: 16 hex over `code`, `source`, `scope.operation`, plus the top application frame (function and root-relative path, no line) for `X_INTERNAL`. Never the cause or an id, so two throws differing in a row id are one group, and a deploy is not a new one. The Sentry envelope sends it as `fingerprint` |
168
+ | Log level | `LOG_LEVEL`, default `info` |
169
+ | Secret redaction | a `Secret` is redacted by value before every other branch, at any depth → [Configuration](Configuration) |
170
+ | Request context | the ALS context carries the request id every log line and span inherits |
171
+ | Live queries | the realtime client puts a `traceparent` on the `subscribe` frame (`LiveClient({ traceparent })`); the snapshot that subscribe causes is the span `live.snapshot <query>`, a CHILD of it, and a re-read a change forces is `live.rerun <query>`, which LINKS to it and parents on the change. A value that is not a valid W3C header is ignored, never refused: telemetry never fails a subscribe. The page boot sends none (`As of 2026-11`): a browser holds no trace |
172
+ | Request span name | `GET /r/:token` — the route PATTERN, never the concrete URL (a token in a path would reach every collector); `http.route` carries the same pattern, and an unmatched request is named by its method alone with `http.route: unmatched` (`As of 22.6.0`) |
173
+
174
+ ## Browser failures
175
+
176
+ `app.config.ts` `clientErrors: { enabled: true }` (`As of 30.0`, off by default) sends a failure on an `app/` page to the same `ErrorReporter` a server failure reaches, as `source: 'browser'` and code `X_CLIENT_ERROR`. `site/` documents never carry it; the admin ships no script.
177
+
178
+ | Concern | Rule |
179
+ |---|---|
180
+ | Doors | window `error` (only a `filename` on this origin; `Script error.` with no stack dropped), `unhandledrejection` (dropped when its stack names only other origins), an island whose `mount()` rejected (the `data-x-failed` marker, watched by a `MutationObserver` — the hydration runtime swallows that rejection, so no window event fires) |
181
+ | Script | `/x-client-errors.js`, 1,961 B (962 B gzip) `As of 2026-11-03`, in `<head>` with `defer`. Served `app/` documents only: the static export `budget.js` is weighed on never names it, so no route figure moves. The build is the document's `x-ultimate-build` meta (else the tag's `data-build`), read at send time. Never throws, never reports its own failure |
182
+ | Caps | 5 reports per document, one per distinct path and message — reset when the client router swaps a page in (`ultimate:navigated`); one report per failed island element, so one carried across a swap is not re-reported; message 1,000, stack 8,000, path 2,048 characters; body 32 KiB |
183
+ | Route | `POST /_client-errors`, `204` on acceptance. `403 X_CSRF_BLOCKED` without proof of same origin (`sec-fetch-site`, then `Origin` — a request with neither is refused too); a path with a query, a fragment, whitespace or a control character is refused; `401` anonymous unless `clientErrors.anonymous: true`; `422 X_BODY_INVALID` for a body over the cap, an unknown key or a field out of shape; `429` past `clientErrors.perMinute` per actor or address, on the app's rate-limit store. A refusal is never reported |
184
+ | Report | `cause` = the browser's message; `stack` = the browser's stack, as sent (no source maps); `meta.clientBuild` beside the server's `release` tells a stale tab from a bug; `meta.kind`, `meta.island`; `scope.operation` = the matched `app/` route pattern, plus the island id when a page of this build declares that island — any other id is filed with no operation, so an invented id makes no new group; `scope.extra.path` = the path, never the query string. `errorFingerprint` groups on these: one island's fault on every `/posts/:id` is one group |
185
+ | Credential | none in the browser: the tab posts to its own origin and the server forwards through the configured reporter |
186
+ | Static export | no route to receive a report, so no exported document names the script and `x build` warns |
187
+ | Not on this route | source maps, a filter DSL — an app's own rules belong in its wrapped `ErrorReporter`. Timings are their own door, below |
188
+
189
+ ## Real-user timings
190
+
191
+ `clientErrors.vitals: { enabled: true }` (`As of 2026-11`, off by default) measures what a user waited for, with no library: `PerformanceObserver` in the browser, one beacon per page view.
192
+
193
+ | Concern | Rule |
194
+ |---|---|
195
+ | Config | `enabled` (default `false`), `sampleRate` (share of page views, 0–1, default `0.1`, decided once per view), `surfaces` (default `['app']`; `'site'` opts the static path in by name). A rate out of range or an unlisted surface refuses the config |
196
+ | Timings | `lcp`, `fcp`, `inp`, `ttfb` (ms from navigation start) and `cls` (largest session window). CLOSED: any other name is dropped |
197
+ | Script | `/x-client-vitals.js`, 1,202 B (633 B gzip) `As of 2026-11-06`, `defer` in `<head>`, on documents of a listed surface only |
198
+ | Beacon | `navigator.sendBeacon('/_client-vitals')` on the first hide (`visibilitychange` → hidden, or `pagehide`), naming the path the view LOADED at |
199
+ | Route | `POST /_client-vitals`. Same origin or dropped; body ≤ 4 KiB; 60 beacons a minute per address; anonymous — no actor on a timing. EVERY outcome is `204`, so a probe learns nothing |
200
+ | Metrics | `web_vital_seconds{route, metric}` (histogram, seconds) and `web_vital_cls{route}`. `route` is the matched route PATTERN on a listed surface, never the URL; an unmatched path records nothing. A timing past 60 s, a CLS past 10 or a negative value is dropped |
201
+ | Budgets | charged, never exempted: an `app/` route is weighed as served and pays the script; a `site/` page is exported carrying it when `site` is listed. On a static host its beacons go nowhere, and `x build` warns |
202
+ | Storage | none. The metric is the record |
203
+
204
+ ## Error groups
205
+
206
+ `errorGroupsSink` (`@ultimat3/admin/error-groups`, `As of 2026-11`) is an `ErrorReporter` that folds every report into its `errorFingerprint` group in `x_error_groups` — first seen, last seen, count, a redacted sample — and `/admin/errors` lists them (`errorGroupsPages`, permission `errors:read`).
207
+
208
+ ```ts
209
+ import { errorGroupsSink, postgresErrorGroupsStore } from '@ultimat3/admin/error-groups';
210
+ import { chainErrorReporters, configureErrorReporting, installedErrorReporter } from '@ultimat3/core';
211
+ import { job, t } from '@ultimat3/jobs';
212
+
213
+ // The app's own: which tracker, which title. Enqueued once per NEW group, fleet-wide.
214
+ export const openErrorIssue = job({
215
+ input: t.object({
216
+ fingerprint: t.string,
217
+ code: t.string,
218
+ source: t.string,
219
+ operation: t.nullable(t.string),
220
+ message: t.string,
221
+ firstSeen: t.string,
222
+ }),
223
+ idempotencyKey: ({ fingerprint }) => `error-issue:${fingerprint}`,
224
+ tenant: 'none',
225
+ retry: { attempts: 3, backoff: 'exponential' },
226
+ async run({ input }) {
227
+ console.log('new error group', input.fingerprint);
228
+ },
229
+ });
230
+
231
+ export const errorGroups = postgresErrorGroupsStore();
232
+ const sink = errorGroupsSink({ store: errorGroups, onNewGroup: openErrorIssue });
233
+ configureErrorReporting({ reporter: chainErrorReporters(installedErrorReporter(), sink) });
234
+ ```
235
+
236
+ | Concern | Rule |
237
+ |---|---|
238
+ | Composing | `chainErrorReporters(...)` from `@ultimat3/core`: every member gets every report, one that throws costs only itself, chains flatten. The container chains its `SENTRY_DSN` transport AFTER whatever the app installed — never over it |
239
+ | "New", once | the upsert's `xmax = 0` decides, so N replicas reporting one new fault enqueue `onNewGroup` once. Which tracker, title and assignee are the app's job |
240
+ | Transactions | written outside the request's transaction: a rollback never erases its own error |
241
+ | Load | `perMinute` store writes per process (default 120); past it, reports are counted and written as one later. A failed write is logged and never re-enters the reporter |
242
+ | Table | `x_error_groups`, applied at boot with every framework table (`@ultimat3/admin/schema`). Retention is the app's: `errorGroupsPurgeTarget(store, { olderThanMs })`, no default |
243
+ | Stores | `postgresErrorGroupsStore()` for a fleet; `memoryErrorGroupsStore()` for tests — it forgets at restart and counts per replica |
244
+
245
+ Running an app for real — the PaaS → Compose → Kubernetes ladder, secrets, dashboards, datastore sizing, DR and runbooks — is the operations manual: [`docs/ops/`](https://github.com/developerz-ai/ultimate/tree/main/docs/ops), and specifically [`docs/ops/03-observability.md`](https://github.com/developerz-ai/ultimate/blob/main/docs/ops/03-observability.md). Recommendations only; the framework depends on none of it.
@@ -0,0 +1,109 @@
1
+ # Outbound HTTP
2
+
3
+ **One call to a vendor: `outbound()` from `@ultimat3/core/outbound`.** A server `fetch(` with no
4
+ `signal` is a build error in a scaffolded app (the `raw-fetch` guard, `boundaries` step) and in this
5
+ repository (`bun run fetch-deadline`, `X_FETCH_UNBOUNDED`). A subpath, never the barrel: core is in
6
+ every bundle graph and the dial screen reads `Bun.dns`.
7
+
8
+ ```ts
9
+ import { outbound } from '@ultimat3/core/outbound';
10
+ import { t } from '@ultimat3/schema';
11
+
12
+ const Charge = t.object({ id: t.string, status: t.string });
13
+
14
+ export async function chargeStatus(id: string): Promise<{ id: string; status: string }> {
15
+ const answer = await outbound({
16
+ url: `https://api.vendor.example/v1/charges/${encodeURIComponent(id)}`,
17
+ vendor: 'vendor',
18
+ dial: 'named',
19
+ deadlineMs: 10_000,
20
+ headers: { authorization: 'Bearer <token>' },
21
+ });
22
+ return answer.json(Charge);
23
+ }
24
+ ```
25
+
26
+ ## What every call gets
27
+
28
+ | Property | Rule |
29
+ |---|---|
30
+ | deadline | `deadlineMs` bounds the WHOLE call, retries and waits included; each attempt also gets what is left of the ambient request's deadline |
31
+ | caller lifetime | the ambient `ctx.signal`, and the call's own `signal`, abort the call and every backoff wait, and the reason reaches you unchanged (`X_DRAINING` stays `X_DRAINING`). A spent ambient deadline is core's `X_TIMEOUT` (`meta.bound: 'caller'`), sent nothing, and counts nothing on a breaker. Inside `runToCompletion(fn, bound)` the request's signal is lifted and the `bound`'s applies: `withIdempotency`'s ceiling ends a hung vendor call |
32
+ | retry | through core's `retry()`. GET, HEAD, PUT, DELETE, OPTIONS: 3 attempts, 250 ms base, 5 s max, full jitter. POST and PATCH: one attempt, unless `idempotent: true` (send the vendor's key header). `retry` on an unkeyed write is `X_OUTBOUND_RETRY_UNSAFE`, before a byte |
33
+ | `Retry-After` | a retryable status that states a delay waits that delay (clamped by the policy's `max`) |
34
+ | body | read under an idle watchdog (`idleMs`, default 30 s) and a byte cap (`maxBytes`, default 16 MiB). `read: 'stream'` hands the body over under both |
35
+ | redirects | `redirect: 'error'` by default. `'follow'` takes at most 5 hops and screens EVERY hop after the first as a public dial, `'named'` included; at an origin change only `accept`, `content-type` and `user-agent` travel |
36
+ | redaction | a cause, `meta` and the `outbound.attempt` log line carry vendor, method, origin and the path's FIRST segment plus the length of the rest (`/services/…(21 chars)`: a webhook keeps its token in the path), never the query or a header. A refusal's body reaches `meta.detail` only as a JSON object, credential-named fields redacted, 200 characters; anything else (a form, an array, HTML, a cut body) is dropped |
37
+ | breaker | `breaker: breaker({ name, failures, cooldownMs })` from `@ultimat3/core`, recorded once per call after the retries. See below |
38
+
39
+ ## `dial`: which network a URL may reach
40
+
41
+ Required, a decision per call.
42
+
43
+ | Value | Use for | What happens |
44
+ |---|---|---|
45
+ | `'named'` | a host the app names in code or config: a vendor API, a sidecar | dialled as written |
46
+ | `'public'` | a URL a tenant or a user supplied | https only outside a local environment, no URL credentials, every address the name answers must be public, the connection pinned to the approved address with `tls.serverName`. Refusal: `X_OUTBOUND_HOST_BLOCKED` |
47
+
48
+ `screenDial(url, { resolve, allowPrivate })` is the screen alone, for a transport that keeps its own
49
+ codes: `@ultimat3/jobs`' webhook target and `@ultimat3/scraping`'s wildcard leg call it.
50
+
51
+ ## Failures
52
+
53
+ | Code | Retry | When |
54
+ |---|---|---|
55
+ | `X_OUTBOUND_TIMEOUT` | retryable | `deadlineMs` passed, or the body went quiet for `idleMs` (`meta.phase`) |
56
+ | `X_OUTBOUND_UNREACHABLE` | retryable | DNS, connect, TLS or reset: no response |
57
+ | `X_OUTBOUND_THROTTLED` | retry-after | a retryable status with `Retry-After` (`meta.retryAfterSeconds`) |
58
+ | `X_OUTBOUND_UNAVAILABLE` | retryable | 5xx, 408, 409, 425, 429 with no stated delay (`meta.status`) |
59
+ | `X_OUTBOUND_REFUSED` | terminal | any other non-2xx (`meta.status`, `meta.detail`) |
60
+ | `X_OUTBOUND_TOO_LARGE` | terminal | the body passed `maxBytes` |
61
+ | `X_OUTBOUND_HOST_BLOCKED` | terminal | `dial: 'public'` refused an address class, a scheme or a hop |
62
+ | `X_OUTBOUND_REDIRECTED` | terminal | a 3xx under `redirect: 'error'`, or more than 5 hops (`meta.location`, no query) |
63
+ | `X_OUTBOUND_RETRY_UNSAFE` | terminal | `retry` on POST or PATCH without `idempotent: true` |
64
+ | `X_OUTBOUND_BODY_INVALID` | terminal | `answer.json(schema)` refused the body (`meta.issues`) |
65
+
66
+ **A 401 is `X_OUTBOUND_REFUSED` with `meta.status: 401`.** There is no refresh hook: the app's own
67
+ wrapper refreshes its token and calls again. Token storage and single-flight stay the app's.
68
+
69
+ ## `breaker()`: stop paying for a dependency that is down
70
+
71
+ ```ts
72
+ import { breaker } from '@ultimat3/core';
73
+ import { outbound } from '@ultimat3/core/outbound';
74
+
75
+ const vendorGate = breaker({ name: 'vendor', failures: 5, cooldownMs: 30_000 });
76
+
77
+ export const ping = () =>
78
+ outbound({
79
+ url: 'https://api.vendor.example/v1/ping',
80
+ vendor: 'vendor',
81
+ dial: 'named',
82
+ deadlineMs: 5_000,
83
+ breaker: vendorGate,
84
+ });
85
+ ```
86
+
87
+ | Rule | |
88
+ |---|---|
89
+ | states | `closed` → `failures` consecutive failures → `open` → after `cooldownMs`, ONE probe (`half-open`) → success closes, failure reopens |
90
+ | a probe that never settles | lapses after `cooldownMs`, so a dead holder cannot wedge it |
91
+ | refusal | `X_BREAKER_OPEN` (retry-after, 503), `meta.name`, `meta.retryAfterSeconds` |
92
+ | trigger | `classify(error)` answers `'failure' \| 'success' \| 'ignore'`. Default: a `retryable` or `retry-after` code is a failure, `X_ABORTED`/`X_DRAINING` ignore, anything else is the dependency answering |
93
+ | tickets | `admit()` answers a ticket or `undefined`; settle with it. Only the probe's own ticket closes or frees a half-open breaker, and an open one ignores a late success |
94
+ | under `outbound()` | timeout, unreachable, unavailable and throttled are failures; refused and the rest are answers; a caller's abort gives the probe back and counts nothing |
95
+ | scope | per process, by design: each process learns in `failures` calls. A shared row would make the breaker depend on the thing most likely to be slow in an outage |
96
+ | metric | `breaker_transitions_total{name,to}` |
97
+ | `name` | a bounded label chosen in code, never a tenant or a URL |
98
+
99
+ Manual use: `const ticket = gate.admit()`, then `succeeded(ticket)`, `failed(ticket)` or `released(ticket)`; or `run(fn)` for all three.
100
+ `state()` never consumes a probe.
101
+
102
+ ## Stays raw, on purpose
103
+
104
+ | Site | Why |
105
+ |---|---|
106
+ | `@ultimat3/jobs` webhook delivery | the job's signal outlives the drain on purpose, and a 3xx is an answer, never a hop |
107
+ | `@ultimat3/scraping`'s HTTP leg | session-bound: browser cookies and a hop chain it owns |
108
+ | `@ultimat3/ai` provider calls | own retry across providers, in-band stream errors |
109
+ | core's OTLP exporter, `x` CLI transports | an exporter must not log through itself; the CLI dials this machine |