@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
@@ -0,0 +1,185 @@
1
+ # Known gaps
2
+
3
+ Defects and unfinished seams named here rather than left to be discovered. Each row carries its own `As of` stamp.
4
+
5
+ A reference manual that hides these is lying to the reader.
6
+
7
+ **Read the section headings first.** [Open](#open) is what is wrong with the code you can install
8
+ today. [Open by decision](#open-by-decision) will not be fixed and says why. [Not built yet](#not-built-yet)
9
+ is unfinished work, not a defect. [Carried backlog](#carried-backlog) is owed work with a tracking
10
+ issue each. [Deferred by plan 101](#deferred-by-plan-101) is ranked work a later plan owns. [Closed](#closed) is history — every row there carries the release
11
+ that fixed it and the workaround for anyone still pinned below it.
12
+
13
+ **Only the [footer](_Footer) stamps the installable version.** Resolve it yourself rather than reading it here:
14
+
15
+ ```bash
16
+ npm view @ultimat3/core version
17
+ ```
18
+
19
+ A Closed row whose release is newer than the [footer](_Footer)'s is fixed in the repository and in **no published release** yet; if you
20
+ are on an earlier version, treat those rows as open and take the workaround. `[Unreleased]` in
21
+ [`CHANGELOG.md`](https://github.com/developerz-ai/ultimate/blob/main/CHANGELOG.md) is the source of
22
+ truth for what the next release carries, and most of what it fixes was never a row here. Read that section rather than this sentence: `grep -n '^## '
23
+ CHANGELOG.md` finds its bounds, and it changes on any commit.
24
+
25
+ **Publication is not a gap.** All 31 workspaces are on the registry `As of 2026-08-20`, checked by `scripts/registry-audit.ts` daily;
26
+ `@ultimat3/scraping` was the last never-published package and was bootstrapped by hand at 2.0.0
27
+ ([`PUBLISHING.md`](https://github.com/developerz-ai/ultimate/blob/main/PUBLISHING.md) step 1).
28
+ `bun add @ultimat3/scraping` resolves — check it with `npm view @ultimat3/scraping version`.
29
+
30
+ ## Open
31
+
32
+ | Gap | Symptom | Work around it by |
33
+ |---|---|---|
34
+ | A purge that a replica never hears is bounded by `ttl`, not by the next request | `As of 2026-10-10`. The purge reaches the other replicas through the invalidation broadcast. With the bus down, a replica with its own store answers its copy as a fresh hit until that copy's `ttl` (and tells an edge the ttl's remainder), then renders. `'purge'` therefore requires a `ttl` | choose the `ttl` as the longest a missed purge may last; or share one `runtime.isrStore` with a `tagFence`, where the delete is every replica's at once |
35
+ | A bust purges by the **route's** tags | `As of 2026-10-10`. A stored `isr` page records no tags of its own, so `post:1` reaches every stored page of a route tagged `post` — one render too many per page, never one too few. Each bust reads the store's keys once; the route of each key is remembered, not re-matched [pin](https://github.com/developerz-ai/ultimate/blob/main/packages/render/src/render-isr-purge.test.ts#every%20stored%20key%20of%20the%20route%20goes%3A%20both%20locales%2C%20and%20every%20keyed%20query) | tag a route as narrowly as its data allows; a very large shared store pays one `paths()` per bust per replica |
36
+ | A declared `isr` query parameter's **values** are unbounded | `As of 2026-10-10`. `query: ['page']` puts `page` in the key, and `?page=1` … `?page=999999` are each a key: the framework cannot know which values name a page. The store is bounded (least recently generated out first), so it thrashes rather than grows [pin](https://github.com/developerz-ai/ultimate/blob/main/packages/render/src/render-isr-query.test.ts#only%20a%20declared%20parameter%20is%20part%20of%20the%20key%2C%20and%20of%20the%20URL%20load%20is%20given) | answer `withStatus(404, …)` from `load` for a value that names nothing, and rate-limit the route at the ingress; put a small closed set in the path instead (`/blog/page/[n]` with `prerender()`) |
37
+ | A `grantUpload` with a `target` is never sniffed | `As of 2026-11-02`. A presigned PUT signs `host` and the size, so on an s3 disk the uploader chooses the stored type and the bytes. `promoteAttachment` is what checks them (`X_STORAGE_TYPE_REJECTED`), and a grant with a `target` lands straight on the row's key with no promotion step. Served, it is contained: `objectResponse` sandboxes a type a browser would run, and a false `image/png` is shown under `nosniff` [pin](https://github.com/developerz-ai/ultimate/blob/main/packages/storage/src/grant.test.ts#naming%20a%20target%20puts%20the%20key%20on%20the%20row%20instead%20of%20in%20pending) | grant with no `target` and call `promoteAttachment({ …, policy })`; for a targeted grant, read the head yourself with `validateStoredHead(…)` before writing the key to the row → [Storage and uploads](Storage-And-Uploads) |
38
+ | A queued offline write is not bound to its principal **on the server** | `As of 2026-10`. The fence is in the page: a principal change abandons the running replay, refuses a write in flight (`X_OFFLINE_QUEUE_ABANDONED`) and wipes the queue. A second TAB whose replay is already past its store read when another tab changes the session cookie still sends its remaining entry under the new session — one entry, one in-flight pass, on a shared browser | sign out by full navigation (`signOutHeaders()` clears site data, and every boot wipes other principals' queues); do not share a browser profile between accounts with offline writes queued |
39
+ | Presence leaves past the lease roster cap | the sweep leader ships at most 4,096 member ids on its lease (`PRESENCE_LEASE_ROSTER_LIMIT`). When the leader's node dies, members beyond that whose TTL expired before the successor's first leading pass are never announced as left. A successor cannot diff against the shared store: the expired members are already gone from it | nothing to do: each client's next beat is answered with the whole roster, so it heals within one beat (10 s by default) |
40
+ | An **offline** scrape reads `/robots.txt` over the real network | every `fakeBrowser` / `fixtureBrowser` run takes the default `robots: 'obey'`, and the gate reads `https://<host>/robots.txt` with the platform `fetch` before the first navigation, so outside `bun test` one request still leaves the process. **The silent half is closed `As of 2026-10-04`**: an unreachable robots.txt (5xx, 429, network error, timeout) now refuses the origin with `X_SCRAPE_ROBOTS_DISALLOWED` (RFC 9309 §2.3.1.4 for 5xx, network error and timeout; refusing on `429` is Ultimate's stricter policy — the RFC calls a `4xx` unavailable), so under `bun test` the sealed network's refusal turns the run red instead of reading as allow-everything | declare the policy in the definition under test — `robots: { ignore: 'fixture host, no live origin' }` — which returns a gate that reads nothing at all. There is no boolean and no ambient off switch, by design — see [Scraping](Scraping#testing-offline) |
41
+ | The cache fill fence crosses nodes for **tag busts through Redis only** | `As of 2026-10`. A read-through fill re-checks a fence before it writes. Inside one process that covers everything. Across nodes: the Redis tier keeps leased generation keys a bust writes and a fill samples before `load()` and re-reads after its `SET` ([`packages/cache/src/redis-fence.ts`](https://github.com/developerz-ai/ultimate/blob/main/packages/cache/src/redis-fence.ts)), so a tag bust on another pod withdraws the fill even when the broadcast is lost — at one extra round trip before each `load()` and one after the `SET`. Three things still do not cross. **Key busts**: `CacheStack.write` and `.drop` mark `{ key }` locally and the wire form carries tags only. **A deployment with no Redis tier**: there is no shared store to hold a generation, so the window is the broadcast's. **Another pod's LRU**: a value it cached before the bust stays until its TTL when the broadcast is lost | give a `cache:` query a TTL you can afford to be wrong for on one pod, keep `redis` in `cache.tiers` on a multi-pod deploy, and prefer a **tag** bust over `cache.drop(key)`: only the first one is broadcast and fenced |
42
+ | A replicated table with **no** replica identity is warned about, never refused | `As of 2026-10-02`. A **channel declared with params** does need `REPLICA IDENTITY FULL` on its `records` tables — a DELETE is routed by the old row's params, which a key-only image does not carry, so members keep the deleted record; `x db gen` grants it since 24.0.0 (one migration for an app that already declares such a channel), and the replicator warns (`replication.channel_identity_partial`) about a table that still lacks it. Live queries do **not** need `REPLICA IDENTITY FULL` on a keyed table: the shared window holds the whole row, so a row leaving the result set and a delete are decided from the window — proved on real WAL by [`pg-identity-window.live.test.ts`](https://github.com/developerz-ai/ultimate/blob/main/packages/realtime/src/pg-identity-window.live.test.ts) (out-of-filter update, into-filter update, delete with a key-only before-image, updates to rows never held). What still breaks is a table with no identity at all — `NOTHING`, or `DEFAULT` with no primary key — whose `UPDATE`/`DELETE` Postgres refuses once published. `X_LIVE_REPLICA_IDENTITY` names exactly those at preflight, and warns rather than throws; no `x verify` step reads it. The `REPLICA IDENTITY FULL` the app's declarations need is checked at the gate: since 24.0.0 the `drift` step reports a params-channel table whose FULL no migration recorded (`X_DB_SCHEMA_UNMIGRATED`) — against the migrations, never the live database. A live query's `subscribes:` tables are **not** granted FULL, `As of 2026-10` (#518): `x db gen` validates the names and emits no `ALTER`; a table already on FULL keeps it, and the drift step reads that as agreement | give the table a primary key, or `ALTER TABLE <t> REPLICA IDENTITY FULL;` — the warning names every table that needs it |
43
+ | HPAs read `<unknown>` without a metrics adapter | the chart half is done — `values.yaml` declares `metricsPort: 9090`, every role but `migrate` emits the container port, `service.yaml` publishes it and `templates/servicemonitor.yaml` ships the scrape target, rendered wherever the cluster serves the Prometheus operator's kind (`serviceMonitor.enabled: auto`, `As of 2026-11`). One thing still stands and it is not the chart's to fix: turning scraped series into the `Pods` metrics an HPA reads needs a **custom-metrics adapter**, which is the cluster's pin: none — what is missing is the cluster's custom-metrics adapter, outside this repository | install a metrics adapter (and the Prometheus operator, if the cluster has none; an app chart from before the scrape object gets it from `x g chart-alerts`); until then disable the HPAs and pin `replicas`. **Do not hand-add a metrics container port** — the chart emits one and a duplicate is rejected by the API server → [Observability](Observability) |
44
+ | `--target binary` is a **launcher** for an app tree, and has never been served from a bare VM | `As of 2026-10-06`. It boots: `x build --target binary` compiles `apps/web/server.ts` with `--compile-autoload-package-json`, `--compile-autoload-tsconfig` and `@ultimat3/*` external (`binaryArgs`, `packages/cli/src/cmd-build.ts`), and the required `windows` CI job starts the result from a scaffold and gets `200` from `/healthz`; `packages/cli/e2e/binary-launch.e2e.test.ts` proves the same on Linux. **It is not a self-contained artifact**: at run time it reads the app's `package.json` and `tsconfig.json`, imports `app.config.ts` and every module under `apps/*`, and loads the framework from the app's `node_modules` — one instance, so the app's registrations reach the server. Unmeasured: a scaffold compiled, copied with its tree to a VM with no Bun, and served under systemd [pin](https://github.com/developerz-ai/ultimate/blob/main/packages/cli/e2e/binary-launch.e2e.test.ts#the%20compiled%20launcher%20resolves%20its%20app%20tree%20at%20run%20time%2C%20against%20ONE%20framework%20instance) | ship the app root, its installed `node_modules` (installed for the target OS and CPU) and the binary together, and start the binary from that root. Prove it for your own app first — `./app --version`, then `ROLE=web PORT=3000 ./app` and `curl /readyz`. Prefer `--target docker`, the self-contained artifact → [Deployment](Deployment) |
45
+ | A local `typecheck` step can report green on a tree CI fails, on the same commit — **one sighting, unreproduced** | `As of 2026-10`. Observed once (2026-09-17, TypeScript 7.0.2): after a `git stash -u` → branch switch → `x verify` on the other branch → switch back → `stash pop` sequence, a type error that surfaces in an UNCHANGED file (a widened index signature in a file it imports) passed locally and failed CI; deleting `tsconfig.tsbuildinfo` then reproduced CI's red locally. Five faithful reproduction attempts against 7.0.2 all stayed correctly red ([#450](https://github.com/developerz-ai/ultimate/issues/450)). **[#450](https://github.com/developerz-ai/ultimate/issues/450) itself is closed `As of 2026-09-23`** — read from the shipped fix, not from the tracker: plan 101 row 14 r ("Close #450", `docs/plans/2026/09/23/101-framework-deep-sweep/14-guards-gate.md:37`; plan `status.yml` `complete`), recorded in `CHANGELOG.md` under 22.0.0 and pinned by `packages/cli/src/verify-typecheck.test.ts`. `tsc -b` decides "up to date" by **mtime**, and Bun installs a dependency as **hardlinks** carrying its cache's old mtimes, so an upgrade to an already-cached version left every input older than the buildinfo and a root without `references` — every scaffolded app — skipped the program and reported green while a cold CI was red. A scaffolded app's `typecheck` is now `tsc -p .` ([`packages/cli/src/templates/scaffold-repo.ts`](https://github.com/developerz-ai/ultimate/blob/main/packages/cli/src/templates/scaffold-repo.ts), its `"typecheck"` script), which is incremental by **content hash**, and `x verify`'s `typecheck` step picks `-p .` over `-b` for any such root ([`packages/cli/src/verify-typecheck.ts`](https://github.com/developerz-ai/ultimate/blob/main/packages/cli/src/verify-typecheck.ts)); the framework root keeps `-b`. **Still open**: a *second* `tsc` racing the same buildinfo is the one suspect not excluded, and no cause for it has been shown. `x verify` deliberately takes two flags and no `--cold` pin: none — one sighting, and five faithful reproductions stayed red, so no test can hold it | CI is always cold and is the judge of record. If a local `typecheck` disagrees with it again, capture the command, its argv and its exit code before deleting any `tsconfig.tsbuildinfo` |
46
+ | A `stream` page with a realtime island keeps every island on the page inert until its slowest hole closes | `As of 2026-11-02`. The runtime goes `async` so a shell island answers while a hole is open, but only with no realtime boot in the document: the boot (`script defer`) has to run first (`bootScript`, [`packages/cli/src/route-islands.ts`](https://github.com/developerz-ai/ultimate/blob/main/packages/cli/src/route-islands.ts)), and a deferred script waits for parsing to end. Bound by the hole's 15 s deadline | keep the slow read out of such a page's holes, or give it a short deadline of its own: `fetch(url, { signal: AbortSignal.timeout(2_000) })` |
47
+ | An open tab can meet a dead island across a deploy | `As of 2026-11-03`. A web process serves only the hashed files it built, so with `assets.retain` unset (the default) a tab holding the previous build's document gets a `no-store` 404 for its stylesheet, or for its island chunk on the first click, after every deploy; during a rolling update a new document that reaches an old pod gets no stylesheet either [pin](https://github.com/developerz-ai/ultimate/blob/main/packages/cli/src/retained-assets-boot.test.ts#undeclared%3A%20nothing%20is%20published%20and%20there%20is%20no%20reader). Declared, a tab survives six deploys with build 0's island alive [evidence](https://github.com/developerz-ai/ultimate/blob/main/packages/cli/e2e/six-deploys.e2e.test.ts#with%20assets.retain%2C%20the%20click%20boots%20build%200%E2%80%99s%20island%3A%20its%20chunk%20200%2C%20no%20404%2C%20an%20update%20offered) | declare `assets: { retain: { disk, builds } }` over a disk every web pod reads → [Deployment](Deployment#hashed-assets-across-a-deploy--assetsretain) |
48
+ | A soft visit waits for the slowest hole | `As of 2026-11-02`. The client router reads `response.text()` ([`packages/render/src/navigation-fetch.ts`](https://github.com/developerz-ai/ultimate/blob/main/packages/render/src/navigation-fetch.ts)) and swaps the whole answer, so holes fill through the re-run scripts and the first paint of an in-app navigation is as late as a full `ssr` render. A full load streams. No incremental parse here [pin](https://github.com/developerz-ai/ultimate/blob/main/packages/render/src/navigation-fetch.test.ts#a%20page%3A%20its%20text%2C%20its%20build%2C%20no-store%20read%20off%20cache-control) | nothing to do in an app; a page that must paint first on a soft visit declares `navigation: 'document'` |
49
+ | A pod evaluates the framework from **source**, one module at a time — ~28 Mi of resident memory per pod that a bundle would not hold | `As of 2026-10-01`, measured on the framework serve graph alone: 1,071 modules from source settle at **80.4 Mi**; the same graph as one `bun build --target=bun` file is 5 modules and settles at **51.8 Mi** — ~34 kB of RSS per source module, the runtime's cost and not the code's. It is the largest saving left after the per-role load and the prebuilt store (a web pod of the demo app settles at 97–102 Mi, a worker at 80–85 Mi). **Not a patch**: a registry is a module instance, so the framework bundled apart from the app is two `@ultimat3/jobs`, two `@ultimat3/entity` — the app registers into one and the boot reads the other. The unit has to be the app **and** the framework as one graph, per role, built inside the image build from a generated entry that imports what `scanAppModules` would, with the `.tsx`/`.scss` loaders as build plugins. Containers only either way (axiom 7). Design note: [`docs/architecture/13-topology-runtime.md`](https://github.com/developerz-ai/ultimate/blob/main/docs/architecture/13-topology-runtime.md#not-built-one-bundle-per-role) pin: none — a resident-memory measurement of a running pod, not a behaviour a unit test can assert | nothing to do in an app. Size a pod's memory request from `process_resident_memory_bytes` on `/metrics`, not from a guess |
50
+ | Every role evaluates all of `@ultimat3/jobs` — 96 modules (`As of 2026-10-02`, by file) — whatever it runs | `As of 2026-10-01`, read off the serve graph by the names each role's code uses: a `web` pod needs 43 of them (enqueue, the outbox relay, the purge task), a `scheduler` 46, a `worker` 62. The rest ride the barrel: `worker.ts` and its eighteen dependencies in a web pod, the `backfill()` / `exportJob()` / `webhook()` factories (17 modules, 123 kB of source) in an app that declares none, and the memory driver (3 modules) outside a test. ~1.6 Mi per web pod at the measured 34 kB a module. Closing it means subpath entries (`@ultimat3/jobs/worker`, one per factory) and every importer rewritten — a breaking move for a saving the per-role bundle above makes moot, so it waits on that decision. The operator surface is **not** in this list: `JobIntrospection` is a member of the driver, and the worker announces itself and counts settles through it pin: none — a module count read off a role's serve graph, measured rather than asserted | nothing to do in an app |
51
+ | A job's `runAt` is stamped from the enqueuer's clock | `As of 2026-10`. An enqueuer running ahead of the database delays its own "immediate" jobs; up to 1 s ahead still wakes a worker, beyond that the job starts on the poll. A delayed job enqueued by another process starts within the idle ceiling after its `runAt` — 5 s with a proven wake, 2 s without; only a retry or a sleep the worker itself handed back gets a due-time timer | NTP on every pod; for a deadline tighter than the ceiling, enqueue at the moment the work is due rather than with `runAt` |
52
+ | `onSettled` runs at most once across a crash | `As of 2026-10`, by decision of the first cut: the hook runs after the settle, inline, by the worker whose settle landed. A worker killed between the settle and the hook does not run it, and nothing re-drives it | record what must be recorded for certain inside the body, in a `step.run`, and use `onSettled` for what may be lost |
53
+ | The CDP driver's `allowHosts` judges NAMES, and a page's WebSocket is not screened at all | `As of 2026-10`. Measured on Chrome 150 with `localBrowser()`: interception (page-level and the browser-level `Fetch` that now covers popups) never pauses a WebSocket handshake, so `new WebSocket('ws://off-list/…')` from a scraped page connects. And the browser resolves its own names, so a host a wildcard admitted that resolves to a private address is reached from the browser leg — the HTTP leg and the robots read resolve and pin (`packages/scraping/src/pinned-host.ts`), the browser cannot be pinned through this port | list exact hosts rather than `'*'` on a scrape that runs where private addresses are reachable, and dial a scrape through an `egress` proxy outside that network |
54
+ | `ScrapeReport.usage.bytesIn` counts the HTTP leg only | `As of 2026-10`. The CDP port subscribes to requests, not to their sizes, so the browser leg's bytes are not in the figure (`packages/scraping/src/usage.ts`) | treat `bytesIn` as a lower bound; a provider's own byte count, returned through the resolver's `cost`, is the billable one |
55
+ | An org-scoped operator's runs list reads `x_jobs` with no index on `tenant_id` | `As of 2026-10`. The jobs dashboard keeps an actor with an `orgId` to its own org's runs through `JobFilter.tenantId`, and `x_jobs.tenant_id` is a plain column: the only index naming it is the idempotency one, led by `name` (`packages/jobs/src/driver-pg-ddl.ts`). A platform operator's list is unaffected [pin](https://github.com/developerz-ai/ultimate/blob/main/packages/jobs/src/driver-pg-sql.test.ts#no%20x_jobs%20index%20leads%20with%20tenant_id%3A%20an%20org-scoped%20runs%20list%20scans) | on a large queue, add `create index on x_jobs (tenant_id, created_at desc, id desc)` in an app migration, or operate the dashboard as a platform operator (an actor with no `orgId`) |
56
+ | Retry-from-step on the jobs dashboard is a free-text field | `As of 2026-10`. `job.retry-from-step` takes `{ step: t.string.min(1) }` (`packages/admin/src/jobs/job-actions.ts`); the run's detail page shows its steps, but the form does not offer them as a choice. | copy the step name from the run's `steps` section on the same page |
57
+ | The admin MCP batch tool takes `ids` only | `As of 2026-10`. A `batch` action is one MCP tool, `admin.action.<name>`, and it takes the rows by id; "all matching" — the list's own filter — is a screen control only | list the rows with `admin.<entity>.list` and its `where`, then pass their ids |
58
+ | No counts on the jobs dashboard's state tabs | `As of 2026-10`. `stats()` counts per queue across every tenant, so a count on a tab would show an org-scoped operator other orgs' numbers; the tabs carry none and the overview's tiles carry the counts, for a platform operator only. A per-tenant count is a second query nothing issues yet | a platform operator reads the overview tiles; an org-scoped operator reads a tab's own page |
59
+ | `x drill` proves one node per role on one machine, and drills no object store and no CDN | `As of 2026-11-06`. The fleet is one `web`, one `worker` (and one `sync` for `nats-down`) on one host, every dependency a local container behind a Bun TCP proxy: a multi-node NATS cluster, a Postgres failover to a real standby, a real S3 bucket and a real CDN in front of the web role are never faulted. `nats-down`'s sync lane is proven on a stand-in node (`packages/testing/src/drill-sync.test.ts`), not on `examples/dummy`, whose transport is in-memory; its client subscribes to nothing by default, so a gap counts only what the node volunteers. CI runs three of the eight (`fault-drills`) [pin](https://github.com/developerz-ai/ultimate/blob/main/packages/testing/src/drills.test.ts#the%20eight%20drills%2C%20once%20each) | the rest by hand: `x drill --list --json`, then `x drill <name> --json` with `docker/test-services.env` loaded. A multi-node or object-store fault is a staging drill of your own, against [`docs/ops/06-runbooks.md`](https://github.com/developerz-ai/ultimate/blob/main/docs/ops/06-runbooks.md) |
60
+
61
+ ## Open by decision
62
+
63
+ Each of these is a defect somebody has already argued about, and the reasoning is why it stays.
64
+
65
+ | Gap | Why it stays |
66
+ |---|---|
67
+ | A local write that died after its last rename is listed with no content type until the key is written again | `As of 2026-10`. `localDriver` commits in four steps (marker, sidecar, bytes, clear marker) and a marker left behind means "re-check this pair". `get()` and `stat()` do re-check against the bytes and answer correctly; `list()` never reads bytes, so it reports that object with no `contentType` and `etag: ''` — honest, and it stays that way until the next `put()` of that key. `stream()` returns bytes only and checks nothing. Two files cannot be committed in one step without changing the on-disk layout |
68
+ | A `delete_resource` preview checks only the policy's actor half | `As of 2026-10-18`. Without `live: true` the framework answers a preview without running the action, so the part of a policy that reads the input never runs; a preview can be granted for a call the commit then refuses. Running the policy would mean running the action. An action that needs an exact preview declares `live` in its input and answers its own |
69
+ | `localDriver` cannot hold a key and a key beneath it (`a` and `a/b`) | `As of 2026-10`. An object is a file at its key, and a POSIX path is a file or a directory. The second `put()` is refused by code (`X_STORAGE_KEY_CONFLICT`) where it used to be a bare `ENOTDIR`; `s3Driver` and `memoryStorageDriver` hold both. Storing objects under suffixed names would lift it, at the price of a layout migration for every existing root — and the keys `grantUpload` and `scopedKey` build never nest an object under another, so the limit is met only by hand-built keys |
70
+ | `insert({ ...row })` of a row whose **nullable** `.sealed()` column was read from the repository stores NULL, silently | a sealed property on a repository row is not enumerable, so a spread leaves it behind (`As of 2026-10`). A required one is refused by name (`X_INVARIANT_VIOLATED`, fix `{ ...row, <col>: row.<col> }`); an absent nullable column is a legal insert, and nothing tells a spread from an omission without making every omission an error. Copy a sealed column by name |
71
+ | A drift `fix:` for a table in `public` does not name its schema | `As of 2026-10-02`. A fix for a table read from any other schema carries `set search_path = "<schema>";`; one for `public` is left unqualified, so it relies on the pasting session resolving `public` first. Postgres' default path is `"$user", public`, so a schema named after the connecting role that holds a same-named table shadows it. Check `show search_path` in that session, or prefix the pasted statement with `set search_path = public;` |
72
+ | Drift never compares an index's `where` **text** | the catalog answers its own rewriting of the expression (`(deleted_at IS NULL)`) where the snapshot holds the author's spelling (`deleted_at is null`), so comparing the strings reports drift on a database that is exactly right. Normalising them means shipping an expression parser to compete with the server's. Three of the four parts of an index **are** compared now — columns, uniqueness, direction, and the predicate's **presence** — so a partial index recreated as a total one and a `desc` index rebuilt ascending are both caught ([`packages/db/src/drift.ts`](https://github.com/developerz-ai/ultimate/blob/main/packages/db/src/drift.ts), `compareIndexes`). `x db gen` compares the text, where both sides are generated. A predicate rewritten by hand into a *different* predicate of the same presence is the residue, and it is invisible: regenerate and read the diff |
73
+ | A long `backfill()` retains one step name per batch | `stepRunner`'s `claimed` Set is the authority for `X_STEP_DUPLICATE` and is deliberately unbounded: a 5M-row sweep at `batch: 250` holds 20,000 short strings (~1–2 MB) for the life of the attempt, released when it ends. `MAX_TRACE_NAMES` bounds the *reported* trace, not the membership set. Bounding the set would let a duplicate step name through after 200 batches, and silently replaying a step is the worse failure. Raise `batch` if the attempt is long enough for the retention to matter |
74
+ | The admin has no one-click select-all | `As of 2026-10`, by decision: the admin ships zero JavaScript (`hydrate: 'never'`), so every control is a link or a native form. A row checkbox joins the batch form through its `form` attribute, "all matching" is a radio whose meaning is the list's URL, and an action's input is its own page. Selecting every row on one page is one click per row. An island would buy that click and cost the admin its zero-script rule, and "all matching" already covers the whole list |
75
+
76
+ ## Not built yet
77
+
78
+ | Open | Where it stands |
79
+ |---|---|
80
+ | Multi-node realtime | the forced-restart benchmarks **are** measured, on one `sync` node over `InProcessTransport` — neither run crossed NATS and neither subscribes to a live query, so no cursor, snapshot or gap-repair path is under test. Fanout, throughput and per-node socket capacity across nodes remain targets → [Realtime](Realtime) |
81
+ | `x db studio` | planned. `X_DB_STUDIO_FAILED` is reserved and not thrown; the command exits `X_NOT_IMPLEMENTED` pointing at `x dev` → the `/_x` db panel. Keeping a `bunx drizzle-kit studio` shell-out would have meant a second schema engine for one subcommand |
82
+ | `api/**/route.ts` | not a file kind — owner decision 1 ([#648](https://github.com/developerz-ai/ultimate/issues/648)), `As of 2026-10`. `api/` holds the action and query projections `defineApi()` collects; a route file there is refused at registration (`X_ROUTE_FILE_INVALID`), and 26.0.0 deleted `ROUTE_FILENAME`'s `api` row ([#709](https://github.com/developerz-ai/ultimate/issues/709)). A wire format an action cannot speak — a webhook receiver, an OAuth endpoint — is a plain HTTP route in the `routes` runtime override ([Configuration](Configuration)) |
83
+ | Deferred, `As of 2026-10-07` | the plugin API and multi-region replication. The Redis **job** driver shipped (`redisJobDriver()`, #710) without the operator surface, backfill bookkeeping or fleet-wide `concurrency` — those answer on Postgres only; the NATS job driver stub that threw `X_NOT_IMPLEMENTED` was deleted in 25.0.0. |
84
+
85
+ ## Carried backlog
86
+
87
+ Rows plan 101 carried from superseded plans and sweeps 10a–10e left open. **The
88
+ backlog row is the record** — cause, evidence and fix live there and in the issue; a row here says
89
+ only what you meet and how to get past it. Backlog: [`10-carried-backlog.md`](https://github.com/developerz-ai/ultimate/blob/main/docs/plans/2026/10/04/101-squeaky-clean-sweep/10-carried-backlog.md).
90
+
91
+ None open, `As of 2026-10-07`: sweep 13 closed the last four — B24 (`agentJob` output, #685), B25
92
+ (`webhook`'s endpoints and ledger, #686), B26 (`t.array` bounds and `transition()`'s row loader, #687)
93
+ and B27 (one `PgExecutor` builder, `dbExecutor()`, #688), in PRs #701, #703 and #706.
94
+
95
+ ## Deferred by plan 101
96
+
97
+ `As of 2026-10`. Gaps the platform-readiness work ranked and did not take, with the evidence that
98
+ ranked them — a later plan's, not this release's. Evidence is from read-only surveys of a
99
+ downstream app on Ultimate and of systems built without it; no row is a defect in what ships.
100
+
101
+ | Gap | Evidence | Precedent |
102
+ |---|---|---|
103
+ | a native form's refusal answer — `setRedirect` covers success (`packages/http/src/redirect.ts`) | 234 wrapper calls in 142 files | Rails' form convention: 303 on success, 422 re-rendering the form with its errors; `flash` |
104
+ | route-group defaults | 111 routes × ~25 repeated lines | layouts, `before_action`, controller inheritance |
105
+ | a view-scoped lazy lookup for `t()` inside an island — the subset itself ships (`catalogSubset` / `subsetTranslator`, #710), named key by key | 94 label types, 3,092 lines of label files | lazy lookup `t('.title')` scoped to the view |
106
+ | a server-first dialog, menu and combobox with a small enhancer | ~2,200 app-side lines; 13 catalog components at 0 imports | HTML first, a small controller second |
107
+ | `QueryRef` derived from the declaration | [Queries and live queries](Queries-And-Live-Queries) "Not derived" row | — |
108
+ | a `task` that runs without a companion job; a boot hook | 35 task/job pairs, 2 boot stand-ins | recurring tasks that name a command; initializers |
109
+ | batches with completion callbacks, debounce, priority, job expiry, argument encryption | each present in a surveyed job system; none asked for by the proving case | batches and limiters in established job systems; queue priorities |
110
+ | proxy inventory, captcha, OTP relay | product features of a scraping service — the app's, by [axiom 8](https://github.com/developerz-ai/ultimate/blob/main/docs/idea/19-mechanism-not-convention.md) | — |
111
+
112
+ The test-kit row of that plan's table shipped in 23.0.0 work (`renderView`, `runJobs({ actor })`,
113
+ `mountIslandState`) and is not repeated here.
114
+
115
+ ## Closed
116
+
117
+ **Historical:** every row is a defect that shipped; the third column is for readers still pinned below
118
+ the release that fixed it. Upgrading is the fix, and the breaking entries are the cost ([Upgrading](Upgrading)).
119
+
120
+ **A row is published once the footer's version reaches its release**; `30.0.0` rows land with that major.
121
+
122
+ | Gap | Fixed in | If you are pinned below that |
123
+ |---|---|---|
124
+ | `bun run error-render` could not see a `catch (error)` binding | **8.0.0** | The gate matched a **parameter** annotated `unknown`/`any`; a `catch (error)` binding is `unknown` by inference and carries no annotation, which is how `String(error)` echoing a caller's request body into a 422 `cause:` stayed green in `packages/http/src/request.ts` through 3.0.0. `error-render` now joins `catchRenderFindings` ([`scripts/catch-render.ts`](https://github.com/developerz-ai/ultimate/blob/main/scripts/catch-render.ts)), which reads the catch binding and a `.catch((error: unknown) => …)` callback; its header lists what it still cannot see (a helper in a third file, a field laundered by another package). Below it: review every `catch` by hand — a caught value reaches a `cause:`/`fix:` only through `renderThrowable(error)` from `@ultimat3/core` |
125
+ | The manifest did not record an admin action's `readonly` or `matching`, a scope's `count`, or a resource's permission noun | **25.0.0** | [B29](https://github.com/developerz-ai/ultimate/blob/main/docs/plans/2026/10/04/101-squeaky-clean-sweep/10-carried-backlog.md), #691. `AdminActionFact` records `readonly` and `matching` and `x manifest diff` reports the gate moving between `admin:write` and `admin:read` ([`packages/manifest/src/diff-admin.ts`](https://github.com/developerz-ai/ultimate/blob/main/packages/manifest/src/diff-admin.ts)); a scope's `count` is diffed; the permission noun reaches the diff through each admin route's `permissions` (`<noun>:read`). Below it: read them from the declaration in `apps/admin/app/admin/admin.ts` |
126
+ | The two-platform deploy proof (milestone 11) was undemonstrated | **22.1.0** | Not a defect in what shipped: the build targets, compose files and chart all shipped, the proof did not. `.github/workflows/ci.yml`'s `deploy-proof` job (`docker/deploy-proof/run.sh`) installs a scaffolded app into kind with `x deploy --method helm` and upgrades it under mixed GET/POST load, failing on one non-2xx ([roadmap](https://github.com/developerz-ai/ultimate/blob/main/docs/idea/14-roadmap.md)). Nothing to work around below it |
127
+ | A withdrawn `isr` page was served once more, however old | **27.8.0** | A tag bust marked the stored page stale and the next request was answered that copy — per stored key, per replica — so a withdrawn article reached its next visitor, days later if nobody asked sooner. `revalidate: { tags, ttl, onInvalidate: 'purge' }` deletes the route's stored pages on a bust and evicts their renders in flight — the next request on that replica, and on each replica the broadcast reaches; one that misses it serves its copy until `ttl` and nothing stale after. `maxStale` bounds a copy served past its `ttl` ([Routes](Routes-And-Render-Modes#how-fast-a-purge-takes-a-page-down)). Below it: serve the route as `render: 'ssr'` with a `cache` age |
128
+ | Any query string minted a stored `isr` page | **30.0.0** | The key carried the whole query, so `/blog?x=1`, `?x=2`, … were each a render and an entry in a store of 1,000, and `utm_*` split one page into many. A route with no `revalidate.query` now keys on none of its query, and `load`, `meta` and the page receive the narrowed URL. Below it: declare `revalidate: { query: ['page'] }` (or `[]`) on every `isr` route, which keys the page on those parameters only; or strip the query at the ingress, or take `render: 'ssr'` |
129
+ | An `isr` page stored a 5xx | **27.7.0** | A `load` answering `withStatus(503)` was stored for the whole `ttl` plus one stale serve, and replaced the good page it was regenerating. A 5xx is now never stored and never replaces a stored page; `noStore(data)` is the loader's own "do not keep this". Below it: throw from `load` on a failed read — a throw was never stored |
130
+ | A page another replica rendered into a shared `isrStore` was not invalidated | **27.8.0** | The invalidation graph only held the pages this process rendered, so a bust arriving on a replica that had merely served a hit found nothing. A bust now also asks the store which keys it holds under the tags, and a store that implements `IsrStore.tagFence` refuses a write from a render that began before the bust on another replica. Below it: give every `isr` route a `ttl` |
131
+ | `/sitemap.xml` answered a 500 when `prerender()` failed at the hourly refresh | **27.7.0** | The running web role forgot the previous sitemap before it re-enumerated. The hourly refresh now runs behind the last good answer, which every request keeps getting; a failed or hung one (30 s) is asked again a minute later, and the third in a row is logged as an error. A `prerender()` that **catches** its failed read and returns `[]` still shrinks the sitemap — let it throw |
132
+ | The edge was purged before the origin dropped the page | **27.7.0** | `invalidateTags` cleared the `cdn` tier first, so a request between the CDN purge and the origin's own delete was a public hit the edge cached again for a whole `s-maxage`. The order is now read tiers → ISR pages → broadcast → `cdn` last. Below it: follow a withdrawal with a second `x cache bust` |
133
+ | A withdrawn `isr` page stayed readable at another spelling of its URL | **27.8.1** | The store key was the raw pathname and the router matches a normalised one. `/blog//a` and `//blog/a` were answered with the page and stored under a key the route table could not read back: no `ttl`, no tags, `s-maxage=60` with a day of `stale-while-revalidate` and no purge key — a tag bust, `'purge'` included, never reached it, at the origin or at the edge, for the life of the process. The key is now the address the router matched ([Routes](Routes-And-Render-Modes)), and a key no route owns is never stored. Below it: normalise the path at the ingress (collapse `//`, strip the trailing slash) or 404 a path containing `//`; restart the pods after a withdrawal |
134
+ | Every spelling of an `isr` URL was its own stored page | **27.8.1** | `/blog/%61bc`, `/blog/abc/` and `/blog/abc` were three entries of one page: a 20-letter slug has a million percent-encodings, against a store of 1,000 with the least recently generated evicted first — any visitor could push the real pages out — and the document stored for a spelling carried that spelling as its `canonical`. A served `static` page's memo (1,024 documents) filled the same way. One entry per address now. Below it: normalise or reject non-canonical paths at the ingress |
135
+ | Every `isr` page under a `basePath` was tag-only and untagged | **27.8.1** | The key kept the mount (`/shop/blog/a`) and the route table has never heard of it, so no route matched: the declared `revalidate.ttl` and `tags` were ignored (`s-maxage=60`, no purge key, no tag edge) and a bust reached nothing. The key now carries no base path. Below it: serve the app at `/` behind a path-rewriting proxy, or take `render: 'ssr'` with a `cache` age |
136
+ | A locale-prefixed `isr` page never revalidated | **27.7.0** | The ISR key keeps the `/en` prefix and the route lookup did not strip it, so `/en/blog/a` matched no route: no `revalidate.ttl` (`s-maxage=60`, the tag-only default), no tag edge. A tag bust and the TTL both expired `/blog/a` and left `/en/blog/a` — a withdrawn post, or a 404 stored before publication — served for the life of the process. Below it: serve the affected route as `render: 'ssr'` with a `cache` age, or restart the pods after a write that should reach a prefixed page |
137
+ | The page outbox did not expose how many writes it holds | **25.2.0** | `useOutbox().size` (`@ultimat3/realtime`) counts the writes queued and not yet taken by the server, and follows every enqueue, ack, refusal and principal change. Below it: count `useMutation`'s `undefined` answers yourself and clear on the browser's `online` event |
138
+ | A tag-only ISR page never went stale | **24.0.0** | No boot attached its ISR controller, so `invalidateTags` reached no revalidator and a `revalidate: { tags }` page with no `ttl` served its first render for the life of the process — on every pod, while `report.isr` listed it as revalidated. `x dev`, the container's web role and `appRoutes()` now attach the controller they build (`packages/cli/src/runtime-isr.ts`) and detach it on stop; [`isr-tag-bust.e2e.test.ts`](https://github.com/developerz-ai/ultimate/blob/main/packages/cli/e2e/isr-tag-bust.e2e.test.ts) proves `x-ultimate-isr: stale` then the new body. Below it, give every `isr` route a `ttl` as well as its `tags` |
139
+ | The ISR cache key was the **pathname**, so a query string was ignored | **4.0.0** | the key is `isrKey(url, locale)` — pathname, the negotiated locale, then the sorted query — so `?q=bob` and `?q=alice` are two documents (declare the parameter: `revalidate: { query: ['q'] }`). Below it, put the varying part in the path (`/search/[q]`) or take `render: 'ssr'` |
140
+ | `invoke()`'s cache bust was not transaction-aware | **24.0.0** | An action invoked inside `withTransaction` busted `cache.invalidates` when its handler returned — before the commit — so a concurrent read refilled the entry from the pre-commit row and the stale value survived to its TTL, and a rollback busted for a write nobody made. The bust is now handed to the root transaction's `onCommit` ([`packages/action/src/cache-gate.ts`](https://github.com/developerz-ai/ultimate/blob/main/packages/action/src/cache-gate.ts)): it fires once the root `COMMIT` is answered, never for a rollback or `X_DB_TRANSACTION_ABORTED`. Below it: do the transactional work *inside* the handler, or drop `cache.invalidates` from the declaration and call `invalidateTags` yourself after the transaction returns |
141
+ | On an s3 disk a grant's `maxBytes` bounded nothing at the upload | **30.0.0** | `grantUpload` requires `request.size` and an s3 disk signs it as the exact `Content-Length`, so the provider answers any other length, chunked included, with 403 `SignatureDoesNotMatch` (proven on the test stack's gateway, `driver-s3.live.test.ts`). `signedUrl({ maxBytes })` with no `contentLength` is refused there (`X_STORAGE_UPLOAD_SIZE_UNDECLARED`) instead of ignored |
142
+ | `X_ROUTE_MODE_INVALID`'s fix offered a `<Suspense>` boundary that does not work | **30.0.0** | It read "wrap the data-dependent part in `<Suspense fallback={…}>` or change render to `'ssr'`", and Solid's `<Suspense>` throws `getContextId cannot be used under non-hydrating context` under this renderer. The boundary check is deleted: a `stream` page needs no boundary, a page with no `<Hole>` is one flush, and the slow part streams through `<Hole fallback>` from `@ultimat3/render`. Below it: take `render: 'ssr'` |
143
+ | `x g` wrote hand-wrapped source and never formatted it | **30.0.0** | `x g` runs the app's own `node_modules/.bin/biome check --write` over the TypeScript files that run created, under the app's config, best effort (`generate-format-written.ts`). A template wrapping wrong for a name no fixture picks reached the app's `lint` as a red gate over code nobody typed. Pinned below it: `bunx biome check --write` over the files the run listed |
144
+ | A deployed scaffold issued no session, so every policy-protected route answered 401 | **30.0.0** | Nothing `x new` wrote called `login()`, and the development viewer answers only under `x dev` — anonymous everywhere else. `x g sign-in --password` or `x g sign-in --oidc <provider>` now writes the sign-in page, the session actions, `auth.signInPath` and a boot guard (`X_AUTH_SIGN_IN_UNCONFIGURED`) → [Auth](Auth). Below it: write the action yourself — `policy: allow('public')`, `const signedIn = await login(auth, { email, password, ip })` from `@ultimat3/auth` over the app's `defineAuth(…)`, then `useRequestContext('createSession').headers.append('set-cookie', signedIn.cookie)` — and post a native form at it |
145
+ | Nothing pinned the documented generator file counts | **4.0.0** | `scripts/generator-counts.ts` reads every page that states how many files `x new` or `x g resource` writes and answers `X_DOC_FILE_COUNT_STALE` on the gate's `manifest` step. Below it, derive rather than quote: `x new myapp --dry-run --json \| jq '.data.files \| length'` |
146
+ | `SessionInit.proxy` was declared on the scrape seam and read by nothing | **23.0.0** | `scrape({ egress: (input) => proxyUrl })` now reaches the driver as `SessionInit.proxy` and wins over the driver's own `proxy`: the launched browser, the HTTP leg and the robots read all dial it, an offline session reports it, and a driver that cannot dial it refuses with `X_SCRAPE_EGRESS_UNSUPPORTED`. On 22.x the exit is a **driver** option — `localBrowser({ proxy })` / `remoteBrowser({ proxy })` — one for every session → [Scraping](Scraping#a-session-a-service-is-built-on) |
147
+ | No test file was typechecked | **24.0.0** | All **31** package `tsconfig.json`s carry `"exclude": ["src/**/*.test.ts"]` — they still do — so `bun run typecheck`, a `tsc -b`, reads none of the **2,195** test files (both counts `As of 2026-10-06`) under `packages/*/src` and the gate's `typecheck` step was green over every one. Re-derive both: `grep -l 'src/\*\*/\*.test.ts' packages/*/tsconfig.json \| wc -l` and `find packages -path '*/src/*' -name '*.test.ts*' -not -path '*/dist/*' \| wc -l`. **Closed by a second program, not by editing the 31:** `tsconfig.tests.json` compiles the tests with `noEmit`, `scripts/test-typecheck-gate.ts` runs it on a per-package ratchet that may only fall, and it rides the gate's `manifest` step ([`scripts/verify.ts:314`](https://github.com/developerz-ai/ultimate/blob/main/scripts/verify.ts)) rather than `typecheck`, which takes no host findings. It landed at **446 errors over 161 files in 27 packages** (measured 2026-08-19) and `TEST_TYPECHECK_PINS` in `scripts/lib/test-typecheck-pins.ts` is now `{}` `As of 2026-10-06`: every package's tests compile at zero errors, so a new test compiles the day it is written. On any published release: nothing to work around at runtime, the tests run either way. Typecheck one package's tests yourself with `bunx tsc --noEmit` over a copy of its config with the `exclude` dropped |
148
+ | A live query re-delivered the raw table row, and mis-ordered a projected window | **5.0.1** | Two defects with one cause: a `ChangeEvent` carries the whole TABLE row, and a live query's result set is whatever its `sql` returned. **The leak** — every patch forwarded the change row unnarrowed, so a column the projection dropped went out on the socket the moment it CHANGED; `examples/dummy`'s feed projects ten columns and one publish delivered `updatedAt`, and a column like a salary or a private note would have gone the same way. The per-subscriber gate could not help: it decides whether a ROW is delivered, never which of its columns. **The mis-ordering** — `match()` decided position by comparing the change row against the rows the WINDOW holds, so an `orderBy` on a column the projection omits measured a real value against nothing: every update read as a move, and an arriving row landed wherever `undefined` sorted. Now a patch row is narrowed to the columns the query actually returned, and a position the window cannot answer for is a `refill` — one re-read and a re-snapshot — rather than a guess. A DELETE still patches incrementally, because it decides no position. [#230](https://github.com/developerz-ai/ultimate/issues/230). On 5.0.0 and below: give a live query's rows the key they are ordered by, and do not rely on a projection to withhold a column from a live subscriber |
149
+ | `jobs.driver` selected no driver | **5.0.0** | [`JobsConfig.driver`](https://github.com/developerz-ai/ultimate/blob/main/packages/core/src/config.ts) accepted `'postgres' \| 'redis' \| 'nats'` and had **no reader anywhere** — boot always built `postgresJobDriver`, and [`packages/jobs/src/driver.ts`](https://github.com/developerz-ai/ultimate/blob/main/packages/jobs/src/driver.ts)'s own header already said so. `jobs: { driver: 'redis' }` therefore did not boot-and-then-throw as this wiki once claimed: it changed nothing and you silently got Postgres — the same shape as `realtime.heartbeatMs`, and worse, because it failed silently in the dangerous direction. Five shipped `fix:` lines named it as the repair for `X_NOT_IMPLEMENTED`, which is a `fix:` that is a no-op; those were corrected in 4.1.0 and the field itself is **deleted in 5.0.0**, along with the `JobsDriver` type nothing else used. On 4.1.0 and below the field still typechecks and still does nothing — swap the driver with `setJobDriver(postgresJobDriver({ executor }))`, or `setJobDriver(memoryJobDriver())` in a test, and never through `app.config.ts` |
150
+ | A caller-controlled `string` could add a line to the 3-line error format | **5.0.0** | `bun run error-render` refuses a parameter typed `unknown`/`any`; a value already typed `string` renders without throwing, so nothing objected — while a newline in one writes a second line an operator, a CI log or the dev overlay's `<pre>` reads as a genuine framework message. Three holes shipped in `@ultimat3/auth` under a green check, the worst reachable by an unauthenticated stranger with one crafted OIDC token. The first fix escaped at each of the six RENDERERS, which could not hold: six is a number that only goes up, and it covered none of the renderers an app writes. Now `UltimateError`'s and `SchemaError`'s **constructors** escape `code`, `title`, `cause`, `fix` and `docs` — so `.message`, `.cause`, `format()`, `toJSON()` and any renderer anyone writes are one line by construction, and `singleLine()` is idempotent so a call site that already escaped is unharmed. On 4.1.0 and below, pass `error.cause` through `singleLine()` from `@ultimat3/core` before you render it yourself |
151
+ | `on delete` was declared and reached no SQL | **4.0.0** | `references(() => orgs.id, { onDelete: 'cascade' })` type-checked and the rule was dropped one layer below the declaration, so no generated `add constraint` ever spelled one and a drift check had nothing truthful to compare. Now: `ColumnDescription`/`ReferenceDescription` carry `onDelete`, `addForeignKey` writes the clause, and a rule changed on either side is `changed-foreign-key` drift whose `fix:` is the drop/add pair. On 3.0.0, add the clause to the `add constraint` the generator emitted, **before** applying the migration: `… references "orgs" ("id") on delete cascade;`. Editing it after it applies moves the checksum → `X_MIGRATION_CONFLICT` |
152
+ | A `references()` **removed** from a column emitted nothing | **4.0.0** | the key stayed on the database, the snapshot beside it recorded `foreignKeys: []` — actively denying a constraint the catalog held — and `compareForeignKeys` judges the declared side, so no check could see it. Now `foreignKeyPlan` emits the `drop constraint` in `up` and the `add` in `down`, naming the constraint the **previous snapshot** recorded rather than the name the generator would have picked. On 3.0.0, write the drop into the next generated migration by hand before applying it: `alter table "posts" drop constraint "posts_org_id_fkey";` |
153
+ | Drift ignored an index's direction and its predicate's presence | **4.0.0** | a `desc` index rebuilt ascending served a feed's newest page off the wrong end, and a partial index recreated as a total one silently widened the constraint — both read `ok: true`. Both are compared now; `asc` is normalised to `null` first, since Postgres stores an ascending index as not-descending. The predicate **text** is still uncompared and always will be — see [Open by decision](#open-by-decision). On 3.0.0, `x db gen` compares all five fields, so regenerate and inspect the diff |
154
+ | An unknown flag was refused **before** a planned command could answer honestly | **4.0.0** | the parser read flags against the spec first, so `x logs tail --follow` reported `X_CLI_BAD_FLAG` instead of `X_NOT_IMPLEMENTED`. A planned command now answers with its own status whatever flags it was given; a **shipped** command still refuses an unknown flag, which is correct — `x env` is shipped and declares `--json`, `--help`, `--cwd` and `--verbose`, so anything else is `X_CLI_BAD_FLAG` for the ordinary reason that no such flag exists. On 3.0.0, run the flagless form of a planned command to see the real message |
155
+ | `x deploy --method helm` threw `X_NOT_IMPLEMENTED` in a scaffolded app | **4.0.0** | `x new` wrote no `docker/helm`, and the command carried a "does this build implement helm?" branch over a build that implemented it completely. `x new` now writes the chart — 8 files, `Chart.yaml` + `values.yaml` + 6 templates — and the lying branch is deleted, so a missing chart is helm's own error. The framework repo's own `docker/helm` carried two more templates the scaffold did not (`pdb.yaml`, `servicemonitor.yaml`); `As of 2026-11` only `pdb.yaml` — the scaffold writes `servicemonitor.yaml` and `alert-rules.yaml`, and `x g chart-alerts` backfills them. On 3.0.0, copy [`docker/helm`](https://github.com/developerz-ai/ultimate/tree/main/docker/helm) from the framework repo, or use `--method compose` |
156
+ | `.env.development` **and `.env.production`** shipped in the image | **4.0.0** | the scaffold's `.dockerignore` excluded `.env` and `.env.*.local` — neither pattern matches `.env.production`, which is the file `docker-compose.prod.yml`'s `env_file:` tells the operator to create. This page said the leak was "harmless as generated"; **that was false**, and a real `docker build` proved both files land in a layer. Now `**/.env` + `**/.env.*` + `!**/.env.example`, in the framework's file, both tracked apps' and the one `x new` writes. On 3.0.0, add those three lines to `docker/Dockerfile.dockerignore` yourself — and rebuild, because an image already built still carries them |
157
+ | `realtime.heartbeatMs` was read by nothing | **4.0.0**, and BREAKING | the key sat in `RealtimeConfig` with a default of 15 000 and no reader anywhere. It is **deleted**: `RealtimeConfig` is `{ enabled, tier, transport, urlEnv }`, the socket beat is the client's `new LiveClient({ heartbeatMs })` and the presence beat is derived (`PresenceRegistry.heartbeatMs` is `max(1000, floor(ttlMs / 3))`). **Refused at boot since 25.0.0**: `defineConfig` throws `X_CONFIG_INVALID` naming `realtime.heartbeatMs` and its replacement (`REMOVED_CONFIG_KEYS`, `packages/core/src/config-removed.ts`), and any other key it does not declare is refused too (`refuseUnknownKeys`, `packages/core/src/config-keys.ts`). From 4.0.0 until 25.0.0 there was no runtime refusal — `section()` copied the key through and only `TS2353` at typecheck caught it. On 3.0.0, delete the key; setting it changes no behaviour either way |
158
+ | `@ultimat3/action`'s `stableStringify` folded `-0`, `NaN` and `Infinity` together | **3.0.0** | this page asked for work that was already done. The hash canonicalizer was split out and moved **down**: `canonicalJson` and `fingerprint` live in `@ultimat3/core` (tier 0), are injective, and give `NaN`, `±Infinity` and `-0` bare tokens of their own plus tagged forms for `Date`, `Map` and `Set`. `requestHash` is `fingerprint(input)`, the job dedupe key is `action:<name>:<fingerprint>`, and `@ultimat3/query`'s copy was **deleted** rather than fixed. `stableStringify` survives as the OpenAPI **document** serializer alone, where emitting valid JSON is the requirement |
159
+ | `.job()` produced a handle nothing could enqueue | **3.0.0** | `agentJob()` in `@ultimat3/ai` is the shipped bridge — an agent as durable, resumable, budgeted background work, with the queue accepting the handle. Pinned by `packages/ai/src/agent-job.test.ts`, whose test is named for this gap: *"agentJob() produces a handle the queue accepts, where .job() never could"* |
160
+ | A branch reaper could drop **another app's** databases | **4.0.0** | `listBranches()` walks `pg_database` for the whole server, so two Ultimate apps on one Postgres plus one nightly sweep was the other app's branches dropped by a `DROP DATABASE` nobody asked for. The marker is now `<base>:<iso>` — `BranchInfo` carries `base` — and `reapBranches` skips any branch whose base is not this database. A **pre-3.x** marker records no base, so it is skipped too, never dropped: self-healing with no migration, because the next `createBranch` writes the base down. On 3.0.0, run the reaper only against a server this app owns |
161
+ | `x db branch list` could not see a branch made before the `psql` shell-out was removed | 2.0.0 | the old path issued `CREATE DATABASE … TEMPLATE` through `psql` and wrote no comment, so a branch made by a 1.2.0 or earlier CLI is absent from `list` — and `delete` may only remove what `list` shows. Nothing back-fills the comment. Drop it by hand from the string the refusal names: `psql "$DATABASE_URL" -c 'DROP DATABASE "<source>_branch_<slug>"'`, where `<slug>` is the branch name with every character outside `[A-Za-z0-9_]` replaced by `_` |
162
+ | MCP tool names were published in `snake_case` and served verbatim | 2.0.0, and BREAKING | `openapi.json` carried `"mcpTool": "publish_post"` while the only name `tools/call` accepts is the export name, `publishPost` — 15 of the 17 `mcpTool` values in the two tracked apps' committed specs named no served tool. `toToolName` is deleted from `@ultimat3/action` and `@ultimat3/query`, all three publishers spell the export name, and `packages/mcp/src/cross-surface.test.ts` drives a `tools/call` with the name OpenAPI published. On 1.2.0, ignore the published value and call the **export name** — `x actions list --json` prints it as `name` → [MCP and AI](MCP-And-AI) |
163
+ | Shared cache tier invalidation `DEL`ed keys it never declared in `KEYS` | 2.0.0 | it failed on **Dragonfly** and on **Redis Cluster** — a cluster cannot route a key it was not told about. The script returns the member list and the tier deletes value keys client-side, one key per `DEL`, so every delete is slot-local. On 1.2.0, single-node Redis, or a cache tier that is not the shared one → [Caching and invalidation](Caching-And-Invalidation) |
164
+ | `resolveEnvironment` existed twice, with different return types | 2.0.0, and BREAKING | `@ultimat3/core` and `@ultimat3/seo` both exported it. seo's is deleted along with `SeoEnvironment`; `RobotsConfig.environment` and `isIndexable()` take core's `Environment`, `'preview'` is spelled `'staging'`, and core gained `tryResolveEnvironment()` for callers that must answer rather than throw — `ULTIMATE_ENV` is in no env schema, so a `robots.txt` render is routinely its first reader. On 1.2.0, import one with an alias → [Configuration](Configuration) |
165
+ | Two head serializers, one of them weaker | 2.0.0, and BREAKING | `@ultimat3/seo`'s `renderHeadTags` escaped `</` only — not `<!--<script>`, which moves the tokenizer into script-data-escaped state — and applied that code rule to a JSON body; **nothing called it**. `@ultimat3/render`'s `renderHead`, the path every `x dev` and every build takes, emitted `<script>` content entirely raw. `renderHeadTags` is gone and `render` escapes raw-text content and JSON-LD by each element's own rule. On 1.2.0, render the head through `renderHead(headFromMeta(meta, seoRenderers()))` and treat any string interpolated into `meta.ld` as untrusted |
166
+ | `docker-compose.prod.yml` paired a published host port with `replicas: 3` | 2.0.0 | one host port has exactly one binder, so the second container died on `Bind for 0.0.0.0:3000 failed: port is already allocated`. `web` and `sync` declare `replicas: 1` in all four files. `sync`'s `PORT` was wrong in the same file and fixed with it — the role binds `PORT + 1`, so `PORT: 3001` opened 3002 while publishing 3001. On 1.2.0, set `replicas: 1`; to scale anyway, drop `ports:` and put your own reverse proxy on the compose network, or climb to the chart's per-role HPA → [`docs/idea/17-scale-ladder.md`](https://github.com/developerz-ai/ultimate/blob/main/docs/idea/17-scale-ladder.md) |
167
+ | `X_MIGRATE_CONCURRENT` was reserved and never thrown | 2.0.0 | the lock was a blocking `pg_advisory_lock`, so an overlapping migrator waited **forever** — no timeout, no exit code, and a wedged predecessor held `helm upgrade --wait` inside one statement with nothing in the logs. `acquireLock` now polls `pg_try_advisory_lock` once per 500ms against a 60s budget (`MIGRATION_LOCK_WAIT_MS`). On 1.2.0, kill the wedged migrator by hand |
168
+ | `x db gen` / `x db migrate` shelled out to `bunx drizzle-kit` | 2.0.0 | which `x new` neither installed nor configured, so `bin/setup` — the scaffold's own documented first command — failed on "drizzle.config.json file does not exist". `gen` calls `generateMigration()` and `migrate`/`reset` call `migrate()`, the engine `ROLE=migrate` already ran. On 1.1.0, generate with `generateMigration` + `writeSchemaHash` from `@ultimat3/db` directly, then apply with `ROLE=migrate bun apps/web/server.ts` |
169
+ | `x new` wrote `0000_initial.sql` with no snapshot sidecar | 2.0.0 | so a scaffolded app started on the one state `x db gen` refuses: `x db migrate` applied it and then answered `X_DB_DRIFT` naming `x db gen`, whose own `X_MIGRATION_SNAPSHOT_MISSING` said "restore from version control" for a file version control never had. `x new` writes no migration at all now — `x db gen` is the one writer of `packages/db/migrations`. On 1.2.0, **before the first migrate**: `rm packages/db/migrations/0000_initial.sql packages/db/migrations/0000_initial.hash && x db gen "initial"` |
170
+ | A generated migration's foreign keys could not apply | 2.0.0 | every key was a `references` clause **inside** `create table`, walked in the app's import order — which says nothing about which table a key points at. Measured against PGlite: `create table "comments" (… references "posts" …)` ran before `create table "posts"`. Every key is its own `alter table … add constraint` after all tables now, and `down` drops constraints before tables. On 1.2.0, move each clause out by hand before the migration is applied |
171
+ | The generated `.snapshot.json` failed the app's own `lint` | 2.0.0 | `JSON.stringify(value, null, 2)` never collapses a one-element array and Biome always does, so `x verify` answered `X_LINT_FAILED` on a file no author typed. The serialiser is a fixed point of Biome 2.5.5 at `lineWidth: 100`, and `x new`'s `biome.json` excludes `**/migrations`. On 1.2.0, add `"!**/migrations"` to `files.includes` |
172
+ | Composite indexes emitted one mangled column name | 2.0.0 | `indexes: [{ on: ['orgId','createdAt'] }]` emitted `on "todos" ("org_id_created_at")`, which will not apply. The description carries the column list, the predicate and the direction, and the generator spells all three. On 1.2.0, write the index by hand |
173
+ | A migration with two statements failed to apply | 2.0.0 | *cannot insert multiple commands into a prepared statement* on the embedded database, and against a server the moment the text carried a bound value. `migrate()` and `rollback()` split with `statementsOf()` and send one statement at a time inside the same transaction. On 1.2.0, one statement per migration file |
174
+ | Every generator but `x g resource` imported files only `x g resource` wrote | 2.0.0 | the templates opened with `import * as repo from '../repo'`, so the generated file did not load: `X_CLI_UNEXPECTED` from every registry command, `--feature <slice>` included. Each generator composes the slice modules its own source imports now, and a module the slice already has is skipped rather than overwritten, `--force` included. On 1.2.0: `x g resource <slice>` **first**, then the narrower generator with `--feature <slice>` |
175
+ | Jobs and tasks registered anonymously | **4.0.0** | a fresh scaffold had no `apps/web/api/index.ts`, so they registered as `anonymous-job-2` / `anonymous-task-1`. `x new` writes it. On 3.0.0, add `defineApi` yourself → [4 · Jobs and realtime](Tutorial-04-Jobs-And-Realtime) |
176
+ | Generated tests landed in the wrong `x verify` step | **4.0.0** | `contractTest` / `liveTest` / `jobTest` inside a plain `*.test.ts` ran under `unit`, and `x test contract` answered `X_TEST_NO_FILES`. The generators emit the typed filename now — `<name>.contract.test.ts` for `x g action`/`x g mutator`, `<name>.live.test.ts` for `x g query --live`, `<name>.job.test.ts` for `x g job`, `x g task` and `x g backfill`. On 3.0.0, rename by hand: the filename is the type |
177
+ | A disk registered under a name that is not its driver's 404'd its own signed URLs | **4.0.0** | `localDriver` minted `/_storage/<driver>/<key>` while the mounted route resolves the segment through the **registry**, so a disk registered as `uploads` signed a URL nothing served. `StorageDriver.registerAs(diskName)` is called by `defineStorage` at boot and the driver hangs its URLs off that; `signedUrlBase` is read off the driver by the verifying half, so the minter and the verifier cannot state it twice. On 3.0.0, register the local disk under the driver's own name (`local`) |
178
+ | `acceptSignedUpload` was reachable from no route | **22.2.0** | the framework mounted only `GET /_storage/:disk/*key`, so every `grantUpload` URL answered 405 (#145, #523). `storageRoutes()` now mounts the `PUT` beside it in `x dev` and `runRole`. It answers `201 { key }` and validates against the grant's own signed constraints. On 22.1.0, mount the `PUT` in your app around `acceptSignedUpload`, or upload through a multipart action |
179
+ | `/_storage` served only the host's disk | **22.2.0** | disks an app declared with `defineStorage` signed URLs that 404'd, and an app with no `storage:read` got a runtime `500 X_PERMISSION_UNKNOWN` (#524). The routes now read the process's one registry, which is the app's when it declares one, and `x verify`'s `policy` step reports the missing permission. On 22.1.0, return the bytes from an action |
180
+ | The robots.txt read left from the worker's IP while every page load left through the proxy | **4.0.0** | two client identities presented to one origin — and an origin reachable **only** through the proxy answered nothing, which the gate reads as "no restrictions". `ScrapeSession.proxy` reports the exit the driver resolved, and `robotsGate` takes it as a **resolver** (`proxy: () => sessionProxy`) because the gate is an argument to `driver.open()` while the exit is decided inside it. The read also carries the run's deadline, its cancellation and a 500 KiB cap. On 3.0.0, use `robots: { ignore: '<reason>' }` on a proxied scrape, or accept that an unreadable robots.txt reads as allow |
181
+ | A malformed request body echoed itself into the 422 | **4.0.0** | the parser's own message quotes the bytes it choked on, and it reached the `cause:` and the log store through `String(error)`. The caller-facing half now names the format alone — *could not parse the body as JSON* — and the parser's message rides in `meta` through `renderThrowable`. On 3.0.0, do not log a 422's `cause` verbatim into anything a third party reads |
182
+ | `rateLimiter({ now })` | **4.0.0**, and BREAKING | renamed to `({ clock })`, the same `Clock` shape `requestContext`'s `init.clock` takes — a second spelling of "what time is it" is a second way to set one number. **The edit:** `rateLimiter({ config, now: () => t })` becomes `rateLimiter({ config, clock: { now: () => new Date(t) } })` |
183
+
184
+ Reserved-but-unthrown error codes are listed in full under [Error codes → Reserved codes](Error-Codes#reserved-codes).
185
+ Symptom-first triage is [Troubleshooting](Troubleshooting).