@ultimat3/cli 29.3.2 → 30.0.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 +13 -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 +28 -8
  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 +42 -138
  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 +17 -27
  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 +1313 -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/src/prerender.ts CHANGED
@@ -16,29 +16,37 @@ import {
16
16
  import type { RouteEntry } from '@ultimat3/render';
17
17
  import { describePages, routeEntries } from '@ultimat3/render';
18
18
  import { renderStatic } from '@ultimat3/render/server';
19
+ import { loadAppConfig } from './app-config-load';
19
20
  import { loadApp } from './app-load';
20
21
  import { appManifest } from './app-manifest';
21
22
  import type { RouteStats } from './budgets';
22
23
  import { measureDocumentJs, routeStatsRow, writeBuildStats } from './budgets';
24
+ import { clientErrorsStaticWarning } from './client-errors-routes';
23
25
  import { errorPageDocument, STATIC_ERROR_PAGE } from './error-pages';
24
26
  import { PrerenderLoadFailedError } from './errors';
25
27
  import { FAVICON_PATH, faviconBytes } from './favicon';
26
- import type { IslandBundle } from './island-bundle';
27
28
  import { buildIslands, writeIslands } from './island-bundle';
28
29
  import { measureDatabase } from './measure-database';
29
30
  import { measurePaths } from './measure-paths';
30
- import { measurementActorLabel, measureScope, withAppUrl } from './measure-scope';
31
+ import { measurementActorLabel, measureScope, publicScope, withAppUrl } from './measure-scope';
31
32
  import { loadNavigation, pageNavigation } from './page-navigation';
32
33
  import { loadSpeculation, otherLocaleSegments, pageSpeculation } from './page-speculation';
34
+ import { asksPrerender, enumerateOnce, type RouteEntries } from './prerender-entries';
35
+ import { heaviestSource } from './prerender-heaviest';
33
36
  import { localizedArtifacts } from './prerender-locales';
34
37
  import { clearPrerenderOut } from './prerender-out';
35
38
  import type { PrerenderedPage, PrerenderReport } from './prerender-report';
39
+ import { collectSeoReads } from './prerender-seo-reads';
36
40
  import type { ServedMeasure } from './prerender-served';
37
41
  import { servedMeasure } from './prerender-served';
42
+ import { clientVitalsStaticWarning, vitalsBuild } from './prerender-vitals';
38
43
  import { loadPwaArtifacts, writePwaIcons } from './pwa-artifacts';
39
44
  import { routeDocument, servedDocument } from './runtime-render';
40
45
  import { writeSiteAssets } from './site-assets';
41
46
  import { loadSiteSettings, originWarning, publicOrigin } from './site-config';
47
+ import type { LlmsPages } from './site-llms-pages';
48
+ import { newestSourceMtime } from './source-mtime';
49
+ import { staticCapture } from './static-capture';
42
50
  import type { SkippedRoute, UnmeasuredRoute } from './static-report';
43
51
  import { skippedRoute, skipReasonFor, writeStaticReport } from './static-report';
44
52
  import { styleBundle, writeStyles } from './style-bundle';
@@ -80,29 +88,8 @@ export interface PrerenderOptions {
80
88
  * on a production build, because a placeholder canonical is one a search engine indexes.
81
89
  */
82
90
  readonly origin?: string;
83
- }
84
-
85
- /**
86
- * The heaviest thing the document actually boots, named by the source file an author can open.
87
- * An island chunk is content-addressed, so its URL says nothing on its own — mapping it back
88
- * through the bundle is what turns `X_BUDGET_EXCEEDED` from a number into an instruction.
89
- */
90
- function heaviestSource(
91
- bundle: IslandBundle,
92
- entries: readonly { readonly url: string; readonly bytes: number }[],
93
- ): readonly string[] | undefined {
94
- const heaviest = entries.reduce<{ url: string; bytes: number } | undefined>(
95
- (max, entry) => (max === undefined || entry.bytes > max.bytes ? entry : max),
96
- undefined,
97
- );
98
- if (heaviest === undefined) return undefined;
99
- const chunk = bundle.chunkAt(heaviest.url);
100
- if (chunk !== undefined) return [chunk.file];
101
- // A shared chunk has no file of its own: the chain is an island on THIS page that imports it.
102
- const importer = bundle.chunks.find(
103
- (one) => one.imports.includes(heaviest.url) && entries.some((entry) => entry.url === one.url),
104
- );
105
- return importer === undefined ? [heaviest.url] : [importer.file, heaviest.url];
91
+ /** Photograph declared social cards. Absent: on unless `ULTIMATE_BUILD_CAPTURE=off` (`--no-capture`). */
92
+ readonly capture?: boolean;
106
93
  }
107
94
 
108
95
  export const DEFAULT_ORIGIN = 'https://localhost';
@@ -130,6 +117,9 @@ export async function prerenderSite(options: PrerenderOptions): Promise<Prerende
130
117
  // The load `x dev` and `x manifest` perform: importing the app's modules fills the route registry.
131
118
  // A module that would not import registered nothing, so its page would vanish from the export
132
119
  // under a green build — refused BEFORE the export is emptied, so the last good artifact survives.
120
+ // Read BEFORE the app loads: a source saved while this build runs is newer than this, so the
121
+ // gate's `budgets` step rebuilds rather than weighing bytes that predate the save.
122
+ const sourcesAt = await newestSourceMtime(options.root);
133
123
  const loaded = await loadApp(options.root);
134
124
  if (loaded.findings.length > 0) throw new PrerenderLoadFailedError(loaded.findings);
135
125
  // Emptied before anything is written: a deleted route's HTML kept shipping when it only grew.
@@ -140,7 +130,14 @@ export async function prerenderSite(options: PrerenderOptions): Promise<Prerende
140
130
  const origin = declaredOrigin ?? DEFAULT_ORIGIN;
141
131
  // Written to stderr as well as returned: an app's `prerender.ts` prints its report as ONE JSON
142
132
  // line on stdout, and a build that parses it must not meet a warning inside it.
143
- const warnings = originWarning(tryResolveEnvironment() ?? DEFAULT_ENVIRONMENT, declaredOrigin);
133
+ const clientErrors = (await loadAppConfig(options.root))?.clientErrors;
134
+ // Real-user timings as served: a site/ page that lists them is exported carrying the script.
135
+ const vitals = vitalsBuild(clientErrors?.vitals);
136
+ const warnings = [
137
+ ...originWarning(tryResolveEnvironment() ?? DEFAULT_ENVIRONMENT, declaredOrigin),
138
+ ...clientErrorsStaticWarning(clientErrors),
139
+ ...clientVitalsStaticWarning(vitals),
140
+ ];
144
141
  for (const warning of warnings) writeErrorLine(`warning: ${warning}`);
145
142
  // The app's locales as `loadApp` read them, ONCE, off `@ultimat3/i18n`'s one reader — the same
146
143
  // answer the PWA manifest and the service worker below are built from. The default first, so
@@ -211,6 +208,10 @@ export async function prerenderSite(options: PrerenderOptions): Promise<Prerende
211
208
  if (navigation.script !== undefined) {
212
209
  await Bun.write(join(options.out, navigation.script.url.slice(1)), navigation.script.code);
213
210
  }
211
+ // Before any site/ document names it, for the router's reason: weighed off disk, and charged.
212
+ if (vitals.siteHead !== undefined && vitals.script !== undefined) {
213
+ await Bun.write(join(options.out, vitals.script.url.slice(1)), vitals.script.code);
214
+ }
214
215
  // The registration TAG now, the worker itself after the render loop — the two halves are wanted
215
216
  // at different moments and used to be taken at the same one. Every document below has to name
216
217
  // `/x-sw-register.js`, and the worker's precache manifest is built from the content hash of
@@ -263,8 +264,9 @@ export async function prerenderSite(options: PrerenderOptions): Promise<Prerende
263
264
  // process, because an `app/` page's `load` calls policy-guarded queries over its typed client
264
265
  // and no server is listening during a build. `renderStatic` keeps `ctx`: its output is a
265
266
  // published file, and a `site/` load a policy refuses must fail the build, never render another
266
- // actor's rows into it.
267
+ // actor's rows into it. `published` authenticates nobody: sitemap and llms.txt reads run in it.
267
268
  const measure = await measureScope({ origin, buildId });
269
+ const published = publicScope({ origin, buildId });
268
270
  // Started on the first route that needs weighing, and released however the loop ends.
269
271
  const database = measureDatabase(options.root);
270
272
  const documentOptions = {
@@ -281,6 +283,7 @@ export async function prerenderSite(options: PrerenderOptions): Promise<Prerende
281
283
  ...(pwa === undefined
282
284
  ? {}
283
285
  : { pwaHead: (locale: string) => pwa.headFor(locale) + (swHead ?? '') }),
286
+ ...(vitals.siteHead === undefined ? {} : { siteHead: vitals.siteHead }),
284
287
  };
285
288
  const render = (entry: RouteEntry, data: { url: string; params: Record<string, string> }) =>
286
289
  routeDocument(entry, data, documentOptions);
@@ -288,7 +291,7 @@ export async function prerenderSite(options: PrerenderOptions): Promise<Prerende
288
291
  // composed on the first route that needs it, so a build weighing none builds no page boot.
289
292
  let weighing: Promise<ServedMeasure> | undefined;
290
293
  const served = (): Promise<ServedMeasure> => {
291
- weighing ??= servedMeasure(options.root, buildId, islands);
294
+ weighing ??= servedMeasure(options.root, buildId, islands, vitals);
292
295
  return weighing;
293
296
  };
294
297
  const document = (
@@ -300,6 +303,11 @@ export async function prerenderSite(options: PrerenderOptions): Promise<Prerende
300
303
  render(entry, data),
301
304
  );
302
305
 
306
+ // The seo seam is installed for the loop and only the loop (`static-capture.ts`).
307
+ const capture = await staticCapture(options.out, options.capture);
308
+ let entries: RouteEntries = new Map();
309
+ const enumerate = enumerateOnce();
310
+ let llmsPages: LlmsPages = new Map();
303
311
  try {
304
312
  for (const entry of routeEntries()) {
305
313
  const facts = { surface: entry.surface, render: entry.config.render, route: entry.path };
@@ -314,10 +322,13 @@ export async function prerenderSite(options: PrerenderOptions): Promise<Prerende
314
322
  try {
315
323
  // A dynamic route is rendered at the paths its own `prerender()` lists, and the row holds
316
324
  // the heaviest — a static route's rule. One that lists none is its own finding.
317
- // Inside the measuring scope: a route's `prerender()` lists its paths by reading the app's
318
- // own data, the same way its `load` does.
325
+ // `prerender()` reads the app's own data as its `load` does — as nobody for a route the
326
+ // sitemap asks, because the one memo hands this answer to `collectSeoReads`.
319
327
  await database.ready();
320
- const plan = await withAppUrl(origin, () => measure.run(() => measurePaths(entry)));
328
+ const enumerateIn = asksPrerender(entry) ? published : measure.run;
329
+ const plan = await withAppUrl(origin, () =>
330
+ enumerateIn(() => measurePaths(entry, enumerate)),
331
+ );
321
332
  if ('unmeasured' in plan) {
322
333
  unmeasured.push(plan.unmeasured);
323
334
  continue;
@@ -413,9 +424,22 @@ export async function prerenderSite(options: PrerenderOptions): Promise<Prerende
413
424
  }
414
425
  if (heaviest !== undefined) routes.push(heaviest);
415
426
  }
427
+ // Before the scope and the database are released: `siteSeo` reads these instead of asking
428
+ // `prerender()` or a page's `load` again, when nothing answers the app's own query any more.
429
+ ({ entries, llmsPages } = await collectSeoReads({
430
+ root: options.root,
431
+ origin,
432
+ defaultLocale,
433
+ scope: (fn) => withAppUrl(origin, () => published(fn)),
434
+ before: () => database.ready(),
435
+ enumerate,
436
+ }));
416
437
  } finally {
438
+ capture.close();
417
439
  await database.close();
418
440
  }
441
+ // The variants and cards the exported documents name, before the worker precaches them.
442
+ await capture.write(pages);
419
443
  // The worker, LAST: every document it precaches has now been rendered, hashed and weighed. A
420
444
  // static host runs no route table, so both files go into the artifact — a
421
445
  // `<script src="/x-sw-register.js">` in every document is a 404 otherwise, which is the same
@@ -437,7 +461,10 @@ export async function prerenderSite(options: PrerenderOptions): Promise<Prerende
437
461
  if (serviceWorker !== undefined) {
438
462
  await Bun.write(join(options.out, SERVICE_WORKER_PATH.slice(1)), serviceWorker.source);
439
463
  }
440
- const stats = await writeBuildStats(options.root, { routes });
464
+ const stats = await writeBuildStats(options.root, {
465
+ routes,
466
+ ...(sourcesAt === undefined ? {} : { sourcesAt }),
467
+ });
441
468
  // Written LAST and by the same call that writes the stats, so an app whose `prerender.ts` does
442
469
  // not reach `prerenderSite` produces neither — and `x verify`'s `budgets` step already reds that
443
470
  // app with `X_BUDGET_UNMEASURED`, which is why this side needs no second code of its own.
@@ -467,5 +494,7 @@ export async function prerenderSite(options: PrerenderOptions): Promise<Prerende
467
494
  serviceWorkerWarnings: serviceWorker?.warnings ?? [],
468
495
  warnings,
469
496
  origin,
497
+ entries,
498
+ llmsPages,
470
499
  };
471
500
  }
@@ -20,7 +20,7 @@ import { join } from 'node:path';
20
20
  import type { PwaColors, PwaOfflineConfig } from '@ultimat3/core';
21
21
  import { escapeHtml, localeSegment, pushWired } from '@ultimat3/core';
22
22
  import type { CacheHint, RequestContext, Route, UltimateRequest } from '@ultimat3/http';
23
- import { applyCacheHeaders } from '@ultimat3/http';
23
+ import { applyCacheHeaders, textResponse } from '@ultimat3/http';
24
24
  import { appLocaleSet } from '@ultimat3/i18n/app-catalogs';
25
25
  import {
26
26
  appleTouchLinks,
@@ -180,7 +180,7 @@ export async function loadPwaArtifacts(root: string): Promise<PwaArtifacts | und
180
180
  * `/assets/pwa/wide.png` — answers the content-hashed URL the site actually serves, exactly as
181
181
  * `asset()` does in a page. The site serves an asset ONLY at its hashed URL, and the manifest took
182
182
  * `src` verbatim, so an app had to hash its own screenshots to avoid shipping a 404 in the install
183
- * sheet (notificado.co's `pwa-install.ts`). A missing file is `X_ASSET_MISSING` at boot, never a
183
+ * sheet (one adopter's `pwa-install.ts`). A missing file is `X_ASSET_MISSING` at boot, never a
184
184
  * broken image in an install prompt; any other `src` passes untouched — an absolute path outside
185
185
  * `/assets/`, an already hashed one, or a shortcut icon's off-site URL (core screens screenshots and
186
186
  * shortcut urls to absolute paths, not icon sources).
@@ -262,7 +262,8 @@ export const pwaManifestRoute = (artifacts: PwaArtifacts): Route => ({
262
262
  },
263
263
  handler: async (_request: UltimateRequest, ctx: RequestContext): Promise<Response> =>
264
264
  applyCacheHeaders(
265
- new Response(manifestIn(artifacts.manifests, ctx.locale)?.body ?? artifacts.body, {
265
+ // `textResponse` for the length it declares: `compress` reads nothing undeclared.
266
+ textResponse(manifestIn(artifacts.manifests, ctx.locale)?.body ?? artifacts.body, {
266
267
  headers: { 'content-type': 'application/manifest+json; charset=utf-8' },
267
268
  }),
268
269
  MANIFEST_CACHE,
@@ -26,7 +26,7 @@ const strings = (value: unknown): readonly string[] =>
26
26
  /**
27
27
  * The manifests' locales, off `appLocaleSet`'s answer (`@ultimat3/i18n/app-catalogs`) — an app
28
28
  * with no catalogs is `UNDECLARED_LOCALES`, the framework default alone. The manifest's `lang` was
29
- * the generator's hardcoded `'en'` for an `es-co` app (notificado.co, 22.3.2). Deduplicated by URL
29
+ * the generator's hardcoded `'en'` for an `es-co` app (one adopter, 22.3.2). Deduplicated by URL
30
30
  * segment, the unit a locale prefix is spelled in.
31
31
  */
32
32
  export function appLocales(set: AppLocaleSet): AppLocales {
package/src/registry.ts CHANGED
@@ -11,21 +11,27 @@ import { authSpec } from './cmd-auth-spec';
11
11
  import { buildSpec } from './cmd-build-spec';
12
12
  import { ciSpec } from './cmd-ci-spec';
13
13
  import { cleanSpec } from './cmd-clean-spec';
14
+ import { clientSpec } from './cmd-client-spec';
14
15
  import { dbSpec } from './cmd-db-spec';
15
16
  import { deploySpec } from './cmd-deploy-spec';
16
17
  import { devSpec } from './cmd-dev-spec';
17
18
  import { docsSpec } from './cmd-docs-spec';
18
19
  import { doctorSpec } from './cmd-doctor-spec';
20
+ import { drillSpec } from './cmd-drill-spec';
19
21
  import { envSpec } from './cmd-env-spec';
20
22
  import { errorsSpec } from './cmd-errors-spec';
23
+ import { exerciseSpec } from './cmd-exercise-spec';
21
24
  import { fixSpec } from './cmd-fix-spec';
22
25
  import { generateSpec } from './cmd-generate-spec';
26
+ import { graphSpec } from './cmd-graph-spec';
23
27
  // Registered here, not by whichever command body happens to load: `dispatch` renders a CLI code's
24
28
  // title for a refusal raised before any body is imported.
25
29
  import './error-codes';
26
30
  import { createHelpCommand, createVersionCommand } from './cmd-help';
27
31
  import { i18nSpec } from './cmd-i18n-spec';
32
+ import { impactSpec } from './cmd-impact-spec';
28
33
  import { jobsSpec } from './cmd-jobs-spec';
34
+ import { mailSpec } from './cmd-mail-spec';
29
35
  import { manifestSpec } from './cmd-manifest-spec';
30
36
  import { mcpSpec } from './cmd-mcp-spec';
31
37
  import { newSpec } from './cmd-new-spec';
@@ -33,11 +39,14 @@ import { plannedCommands } from './cmd-planned';
33
39
  import { policySpec } from './cmd-policy-spec';
34
40
  import { prSpec } from './cmd-pr-spec';
35
41
  import { actionsSpec, entitiesSpec, queriesSpec } from './cmd-registries-spec';
42
+ import { reproSpec } from './cmd-repro-spec';
36
43
  import { routesSpec } from './cmd-routes-spec';
37
44
  import { secretsSpec } from './cmd-secrets-spec';
38
45
  import { shotSpec } from './cmd-shot-spec';
46
+ import { stopSpec } from './cmd-stop-spec';
39
47
  import { tasksSpec } from './cmd-tasks-spec';
40
48
  import { testSpec } from './cmd-test-spec';
49
+ import { upgradeSpec } from './cmd-upgrade-spec';
41
50
  import { vapidSpec } from './cmd-vapid-spec';
42
51
  import { verifySpec } from './cmd-verify-spec';
43
52
  import type { CliCommand } from './command';
@@ -78,6 +87,7 @@ const lazy = (spec: CommandSpec, load: () => Promise<CliCommand>): LazyCommand =
78
87
  export const LAZY_COMMANDS: readonly LazyCommand[] = [
79
88
  lazy(newSpec, async () => (await import('./cmd-new')).newCommand),
80
89
  lazy(devSpec, async () => (await import('./cmd-dev')).devCommand),
90
+ lazy(stopSpec, async () => (await import('./cmd-stop')).stopCommand),
81
91
  lazy(buildSpec, async () => (await import('./cmd-build')).buildCommand),
82
92
  lazy(testSpec, async () => (await import('./cmd-test')).testCommand),
83
93
  lazy(verifySpec, async () => (await import('./cmd-verify')).verifyCommand),
@@ -99,14 +109,22 @@ export const LAZY_COMMANDS: readonly LazyCommand[] = [
99
109
  lazy(jobsSpec, async () => (await import('./cmd-jobs')).jobsCommand),
100
110
  lazy(tasksSpec, async () => (await import('./cmd-tasks')).tasksCommand),
101
111
  lazy(policySpec, async () => (await import('./cmd-policy')).policyCommand),
112
+ lazy(exerciseSpec, async () => (await import('./cmd-exercise')).exerciseCommand),
113
+ lazy(drillSpec, async () => (await import('./cmd-drill')).drillCommand),
102
114
  lazy(i18nSpec, async () => (await import('./cmd-i18n')).i18nCommand),
115
+ lazy(mailSpec, async () => (await import('./cmd-mail')).mailCommand),
103
116
  lazy(errorsSpec, async () => (await import('./cmd-errors')).errorsCommand),
117
+ lazy(reproSpec, async () => (await import('./cmd-repro')).reproCommand),
104
118
  lazy(docsSpec, async () => (await import('./cmd-docs')).docsCommand),
105
119
  lazy(fixSpec, async () => (await import('./cmd-fix')).fixCommand),
106
120
  lazy(affectedSpec, async () => (await import('./cmd-affected')).affectedCommand),
107
121
  lazy(shotSpec, async () => (await import('./cmd-shot')).shotCommand),
108
122
  lazy(prSpec, async () => (await import('./cmd-pr')).prCommand),
109
123
  lazy(ciSpec, async () => (await import('./cmd-ci')).ciCommand),
124
+ lazy(upgradeSpec, async () => (await import('./cmd-upgrade')).upgradeCommand),
125
+ lazy(clientSpec, async () => (await import('./cmd-client')).clientCommand),
126
+ lazy(graphSpec, async () => (await import('./cmd-graph')).graphCommand),
127
+ lazy(impactSpec, async () => (await import('./cmd-impact')).impactCommand),
110
128
  ];
111
129
 
112
130
  /**
@@ -0,0 +1,65 @@
1
+ // Single responsibility: what `x repro <id>` is about — a dead job (`x_jobs` and its steps, with or
2
+ // without capture), a captured request (`x_failures` by bundle, request or trace id, else the newest
3
+ // bundle of a fingerprint), or a bundle file (`--from`). Reads only; refuses with `X_REPRO_*`.
4
+
5
+ import type { FailureBundle } from '@ultimat3/core';
6
+ import { isJsonObject } from '@ultimat3/core';
7
+ import { postgresFailureStore } from '@ultimat3/db';
8
+ import type { JobDriver } from '@ultimat3/jobs';
9
+ import { inspectJob } from '@ultimat3/jobs';
10
+ import type { JobFailure } from '@ultimat3/testing';
11
+ import { ReproCaptureOffError, ReproUnknownError } from './repro-errors';
12
+ import { jobFailureOf } from './repro-job';
13
+
14
+ export type ReproSubject =
15
+ | { readonly kind: 'job'; readonly failure: JobFailure }
16
+ | { readonly kind: 'request'; readonly bundle: FailureBundle };
17
+
18
+ /** A uuid in any spelling `x_jobs.id` accepts — `cmd-jobs.ts`' rule, so no statement is wasted. */
19
+ const JOB_ID = /^\{?[0-9a-f]{8}-?[0-9a-f]{4}-?[0-9a-f]{4}-?[0-9a-f]{4}-?[0-9a-f]{12}\}?$/i;
20
+
21
+ const isBundle = (value: unknown): value is FailureBundle =>
22
+ isJsonObject(value) &&
23
+ typeof value['name'] === 'string' &&
24
+ (value['primitive'] === 'action' || value['primitive'] === 'query') &&
25
+ Array.isArray(value['reads']);
26
+
27
+ const isJobFailure = (value: unknown): value is JobFailure =>
28
+ isJsonObject(value) &&
29
+ typeof value['jobId'] === 'string' &&
30
+ typeof value['name'] === 'string' &&
31
+ Array.isArray(value['steps']);
32
+
33
+ /** `--from <file>`: a bundle `x repro <id> --json` printed (`data.bundle` or `data.failure`), or one bare. */
34
+ export async function subjectFromFile(path: string): Promise<ReproSubject> {
35
+ const file = Bun.file(path);
36
+ if (!(await file.exists())) throw new ReproUnknownError({ id: path });
37
+ const parsed: unknown = await file.json();
38
+ const data = isJsonObject(parsed) && isJsonObject(parsed['data']) ? parsed['data'] : parsed;
39
+ const inner = isJsonObject(data) ? (data['bundle'] ?? data['failure'] ?? data) : data;
40
+ if (isBundle(inner)) return { kind: 'request', bundle: inner };
41
+ if (isJobFailure(inner)) return { kind: 'job', failure: inner };
42
+ throw new ReproUnknownError({ id: path });
43
+ }
44
+
45
+ /**
46
+ * A job first — its row exists whether or not capture is on — then the captured bundles. Not found
47
+ * anywhere: `X_REPRO_CAPTURE_OFF` when the app never stored bundles, `X_REPRO_UNKNOWN` otherwise.
48
+ */
49
+ export async function resolveSubject(
50
+ driver: JobDriver,
51
+ id: string,
52
+ captureOn: boolean,
53
+ ): Promise<ReproSubject> {
54
+ if (JOB_ID.test(id)) {
55
+ const trace = await inspectJob(driver, id);
56
+ if (trace !== undefined) {
57
+ return { kind: 'job', failure: jobFailureOf(trace, await driver.steps.list(trace.runId)) };
58
+ }
59
+ }
60
+ const store = postgresFailureStore();
61
+ const bundle = (await store.find(id)) ?? (await store.latest(id));
62
+ if (bundle !== undefined) return { kind: 'request', bundle };
63
+ if (!captureOn) throw new ReproCaptureOffError({ id });
64
+ throw new ReproUnknownError({ id });
65
+ }
@@ -0,0 +1,47 @@
1
+ // The three ways `x repro` refuses: an id nothing holds, a capture that was never on, and a written
2
+ // test that ran and did not fail the way the original did. Registered in `error-codes.ts`.
3
+
4
+ import { renderFixShellArg, UltimateError } from '@ultimat3/core';
5
+
6
+ export class ReproUnknownError extends UltimateError {
7
+ constructor(input: { readonly id: string }) {
8
+ super({
9
+ code: 'X_REPRO_UNKNOWN',
10
+ cause: `no dead job, captured request, trace or fingerprint matches "${input.id}"`,
11
+ fix: 'x repro --list --json',
12
+ meta: { id: input.id },
13
+ });
14
+ }
15
+ }
16
+
17
+ export class ReproCaptureOffError extends UltimateError {
18
+ constructor(input: { readonly id: string }) {
19
+ super({
20
+ code: 'X_REPRO_CAPTURE_OFF',
21
+ cause: `"${input.id}" is no job, and failureCapture is disabled in app.config.ts, so no request bundle was ever stored`,
22
+ fix: 'x jobs list --json # a job id replays with capture off; for requests set failureCapture: { enabled: true } in app.config.ts',
23
+ meta: { id: input.id },
24
+ });
25
+ }
26
+ }
27
+
28
+ export interface NotReproducedInput {
29
+ readonly id: string;
30
+ readonly expected: string;
31
+ readonly got: string | null;
32
+ readonly masked: readonly string[];
33
+ readonly file: string;
34
+ readonly build: string | null;
35
+ }
36
+
37
+ export class ReproNotReproducedError extends UltimateError {
38
+ constructor(input: NotReproducedInput) {
39
+ const masked = input.masked.length === 0 ? 'none' : input.masked.join(', ');
40
+ super({
41
+ code: 'X_REPRO_NOT_REPRODUCED',
42
+ cause: `${input.file} ended in ${input.got ?? 'a pass'}, not ${input.expected}; masked fields: ${masked}; captured on build ${input.build ?? 'unknown'}`,
43
+ fix: `x repro ${renderFixShellArg(input.id, '<id>')} --json, then set the listed masked fields and unread rows by hand in ${renderFixShellArg(input.file, '<the written test>')}`,
44
+ meta: { ...input },
45
+ });
46
+ }
47
+ }
@@ -0,0 +1,72 @@
1
+ // Single responsibility: a dead job as a replay — what `inspectJob` and the run's step records say,
2
+ // folded into the `JobFailure` `reproJob` takes, and the `.job.test.ts` that replays it. Pure: the
3
+ // reads are `repro-bundle.ts`', the write is `repro-write.ts`'.
4
+
5
+ import type { ErrorReport } from '@ultimat3/core';
6
+ import { captureValue, errorFingerprint, singleLine } from '@ultimat3/core';
7
+ import type { JobTrace, StepRecord } from '@ultimat3/jobs';
8
+ import type { JobFailure } from '@ultimat3/testing';
9
+ import { tsLiteral } from './repro-literal';
10
+
11
+ const CODE = /X_[A-Z0-9_]+/;
12
+
13
+ /** `inspectJob`'s trace (input already masked as `x jobs show` masks it) plus the run's steps. */
14
+ export function jobFailureOf(trace: JobTrace, records: readonly StepRecord[]): JobFailure {
15
+ const { value, masked } = captureValue(trace.input);
16
+ return {
17
+ jobId: trace.id,
18
+ name: trace.name,
19
+ input: value,
20
+ masked,
21
+ // Completed steps only, in the order the run took them: each one replays from its output.
22
+ steps: records
23
+ .filter((record) => record.status === 'completed')
24
+ .map((record) => ({ name: record.name, output: record.output ?? null })),
25
+ code: CODE.exec(trace.lastError ?? '')?.[0] ?? null,
26
+ step: records.find((record) => record.status === 'failed')?.name ?? null,
27
+ };
28
+ }
29
+
30
+ /** The group a job failure reports under — `source: 'job'`, the job's name as the operation. */
31
+ export function jobFingerprint(failure: JobFailure): string | null {
32
+ if (failure.code === null) return null;
33
+ const report: Pick<ErrorReport, 'code' | 'source' | 'scope'> = {
34
+ code: failure.code,
35
+ source: 'job',
36
+ scope: { operation: failure.name },
37
+ };
38
+ // `errorFingerprint` reads three fields through total accessors; the rest of a report is not read.
39
+ return errorFingerprint(report as ErrorReport);
40
+ }
41
+
42
+ export interface ReproSourceMeta {
43
+ /** Bare id, as the user typed it — the comment says how the file came to be. */
44
+ readonly id: string;
45
+ /** Module specifiers imported for their registrations (the app's API index), relative. */
46
+ readonly imports: readonly string[];
47
+ }
48
+
49
+ export const header = (lines: readonly string[]): string =>
50
+ lines.map((line) => `// ${singleLine(line)}`).join('\n');
51
+
52
+ /** A `.job.test.ts` that is RED until the fault is fixed: the run must settle with no error. */
53
+ export function jobTestSource(failure: JobFailure, meta: ReproSourceMeta): string {
54
+ const masked = failure.masked.length === 0 ? 'none' : failure.masked.join(', ');
55
+ return `${header([
56
+ `x repro ${meta.id}: job "${failure.name}" died with ${failure.code ?? 'an uncoded failure'} at step "${failure.step ?? '(the body)'}".`,
57
+ 'Written by `x repro --write`. Red until the fault is fixed: the completed steps replay from',
58
+ `their stored output, then the job runs once more. Masked input fields (${masked}) are stood in`,
59
+ 'for from the schema — set them here when the failure needs their real shape.',
60
+ ])}
61
+
62
+ import { expect, type JobFailure, jobTest, reproJob } from '@ultimat3/testing';
63
+ ${meta.imports.map((specifier) => `import '${specifier}';`).join('\n')}
64
+
65
+ const FAILURE: JobFailure = ${tsLiteral(failure)};
66
+
67
+ jobTest(${tsLiteral(`repro ${meta.id.slice(0, 8)}: ${failure.name} settles`)}, async ({ runJobs }) => {
68
+ const outcome = await reproJob(runJobs, FAILURE);
69
+ expect(outcome.failure).toBeNull();
70
+ });
71
+ `;
72
+ }
@@ -0,0 +1,41 @@
1
+ // Single responsibility: a JSON value as a TypeScript literal in the repo's own style — single
2
+ // quotes, two spaces, trailing commas, keys bare where they can be — so a test `x repro --write`
3
+ // emits passes `lint` without a formatter and reads like a test someone wrote.
4
+
5
+ const BARE_KEY = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
6
+
7
+ const quoted = (text: string): string =>
8
+ `'${text
9
+ .replaceAll('\\', '\\\\')
10
+ .replaceAll("'", "\\'")
11
+ .replaceAll('\n', '\\n')
12
+ .replaceAll('\r', '\\r')
13
+ .replaceAll('
', '\\u2028')
14
+ .replaceAll('
', '\\u2029')}'`;
15
+
16
+ /** `__proto__` written bare or quoted SETS the prototype; only a computed key stays a key. */
17
+ const keyOf = (key: string): string =>
18
+ key === '__proto__' ? `[${quoted(key)}]` : BARE_KEY.test(key) ? key : quoted(key);
19
+
20
+ /** `value` rendered at `depth` (its first line is not indented; nested lines are). */
21
+ export function tsLiteral(value: unknown, depth = 0): string {
22
+ if (value === null || value === undefined) return 'null';
23
+ if (typeof value === 'string') return quoted(value);
24
+ if (typeof value === 'number') return Number.isFinite(value) ? String(value) : 'null';
25
+ if (typeof value === 'boolean') return String(value);
26
+ const pad = ' '.repeat(depth + 1);
27
+ const close = ' '.repeat(depth);
28
+ if (Array.isArray(value)) {
29
+ if (value.length === 0) return '[]';
30
+ return `[\n${value.map((entry) => `${pad}${tsLiteral(entry, depth + 1)},`).join('\n')}\n${close}]`;
31
+ }
32
+ if (typeof value === 'object') {
33
+ const entries = Object.entries(value);
34
+ if (entries.length === 0) return '{}';
35
+ const lines = entries.map(
36
+ ([key, entry]) => `${pad}${keyOf(key)}: ${tsLiteral(entry, depth + 1)},`,
37
+ );
38
+ return `{\n${lines.join('\n')}\n${close}}`;
39
+ }
40
+ return 'null';
41
+ }
@@ -0,0 +1,48 @@
1
+ // Single responsibility: a captured request bundle as the `.contract.test.ts` that replays it —
2
+ // rows seeded through the app's own driver, the clock frozen at the failure, the actor and input
3
+ // as captured. Pure: the read is `repro-bundle.ts`', the write `repro-write.ts`'.
4
+
5
+ import type { FailureBundle } from '@ultimat3/core';
6
+ import type { ReproSourceMeta } from './repro-job';
7
+ import { header } from './repro-job';
8
+ import { tsLiteral } from './repro-literal';
9
+
10
+ export interface RequestSourceMeta extends ReproSourceMeta {
11
+ /** `@<app>/db` — the package exporting `driver`; `null` when the app has none (no rows seeded). */
12
+ readonly dbPackage: string | null;
13
+ /** The build that is about to run the test, to say so when it is not the captured one. */
14
+ readonly build: string | null;
15
+ }
16
+
17
+ export function requestTestSource(bundle: FailureBundle, meta: RequestSourceMeta): string {
18
+ const masked = bundle.masked.length === 0 ? 'none' : bundle.masked.join(', ');
19
+ const drift =
20
+ bundle.build !== null && meta.build !== null && bundle.build !== meta.build
21
+ ? `Captured on build ${bundle.build}, replayed on ${meta.build}: a different outcome may be the fix already shipped.`
22
+ : `Captured on build ${bundle.build ?? 'unknown'}.`;
23
+ const lines = [
24
+ `x repro ${meta.id}: ${bundle.primitive} "${bundle.name}" answered ${bundle.code} (fingerprint ${bundle.fingerprint}).`,
25
+ 'Written by `x repro --write`. Red until the fault is fixed: the rows it read are seeded, the',
26
+ 'clock is frozen at the failure, and it runs again as the same actor with the same input.',
27
+ `Masked input fields (${masked}) are stood in for from the schema. ${drift}`,
28
+ ...(bundle.truncated
29
+ ? ['The bundle was truncated at capture: some rows it read are not here.']
30
+ : []),
31
+ ];
32
+ const driverImport =
33
+ meta.dbPackage === null ? '' : `import { driver } from '${meta.dbPackage}';\n`;
34
+ const options = meta.dbPackage === null ? '' : ', { driver }';
35
+ return `${header(lines)}
36
+
37
+ import type { FailureBundle } from '@ultimat3/core';
38
+ import { expect, reproduce, test } from '@ultimat3/testing';
39
+ ${driverImport}${meta.imports.map((specifier) => `import '${specifier}';`).join('\n')}
40
+
41
+ const BUNDLE: FailureBundle = ${tsLiteral(bundle)};
42
+
43
+ test(${tsLiteral(`repro ${meta.id.slice(0, 8)}: ${bundle.name} answers without ${bundle.code}`)}, async () => {
44
+ const outcome = await reproduce(BUNDLE${options});
45
+ expect(outcome.failure).toBeNull();
46
+ });
47
+ `;
48
+ }