@ultimat3/cli 29.4.0 → 30.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (448) hide show
  1. package/CLAUDE.md +64 -40
  2. package/README.md +61 -11
  3. package/package.json +42 -28
  4. package/src/affected-tests.ts +97 -0
  5. package/src/affected-unowned.ts +43 -0
  6. package/src/affected.ts +25 -6
  7. package/src/app-boundaries.ts +29 -1
  8. package/src/app-evals.ts +12 -4
  9. package/src/app-load-unresolved.ts +62 -0
  10. package/src/app-load.ts +30 -37
  11. package/src/app-mcp.ts +14 -2
  12. package/src/app-module-graph.ts +232 -0
  13. package/src/app-reload-graph.ts +2 -2
  14. package/src/app-root.ts +37 -1
  15. package/src/bin.ts +1 -0
  16. package/src/boundary-findings.ts +6 -0
  17. package/src/budgets-freshness.ts +32 -0
  18. package/src/budgets.ts +20 -1
  19. package/src/build-capture-env.ts +9 -0
  20. package/src/cdp-shot-a11y.ts +2 -39
  21. package/src/ci-tested.ts +243 -0
  22. package/src/client-emit-customer-runtime.ts +139 -0
  23. package/src/client-emit-customer.ts +108 -0
  24. package/src/client-emit-drift.ts +72 -0
  25. package/src/client-emit-header.ts +67 -0
  26. package/src/client-emit-schemas.ts +175 -0
  27. package/src/client-emit-types.ts +121 -0
  28. package/src/client-emit-vendor.ts +146 -0
  29. package/src/client-errors-routes.ts +229 -0
  30. package/src/client-errors-script.ts +74 -0
  31. package/src/client-vitals-route.ts +197 -0
  32. package/src/client-vitals-script.ts +56 -0
  33. package/src/cmd-affected.ts +23 -0
  34. package/src/cmd-build-spec.ts +7 -1
  35. package/src/cmd-build.ts +35 -9
  36. package/src/cmd-ci-spec.ts +42 -6
  37. package/src/cmd-ci.ts +99 -2
  38. package/src/cmd-client-customer.ts +134 -0
  39. package/src/cmd-client-source.ts +81 -0
  40. package/src/cmd-client-spec.ts +48 -0
  41. package/src/cmd-client-write.ts +41 -0
  42. package/src/cmd-client.ts +111 -0
  43. package/src/cmd-db-recount.ts +120 -0
  44. package/src/cmd-db-spec.ts +36 -6
  45. package/src/cmd-db.ts +11 -3
  46. package/src/cmd-deploy-compose.ts +61 -3
  47. package/src/cmd-deploy-drain.ts +10 -2
  48. package/src/cmd-deploy-helm.ts +11 -0
  49. package/src/cmd-deploy-preflight.ts +118 -0
  50. package/src/cmd-deploy-spec.ts +58 -6
  51. package/src/cmd-deploy-verify.ts +380 -0
  52. package/src/cmd-deploy.ts +42 -9
  53. package/src/cmd-docs.ts +34 -4
  54. package/src/cmd-doctor.ts +66 -9
  55. package/src/cmd-drill-spec.ts +25 -0
  56. package/src/cmd-drill.ts +176 -0
  57. package/src/cmd-env-init.ts +102 -0
  58. package/src/cmd-env-spec.ts +10 -3
  59. package/src/cmd-env.ts +15 -10
  60. package/src/cmd-errors-recent.ts +60 -0
  61. package/src/cmd-errors-spec.ts +9 -1
  62. package/src/cmd-errors.ts +4 -0
  63. package/src/cmd-exercise-spec.ts +19 -0
  64. package/src/cmd-exercise.ts +129 -0
  65. package/src/cmd-generate-spec.ts +9 -1
  66. package/src/cmd-generate.ts +32 -27
  67. package/src/cmd-graph-load.ts +97 -0
  68. package/src/cmd-graph-spec.ts +16 -0
  69. package/src/cmd-graph.ts +73 -0
  70. package/src/cmd-i18n.ts +4 -1
  71. package/src/cmd-impact-spec.ts +13 -0
  72. package/src/cmd-impact.ts +64 -0
  73. package/src/cmd-jobs-resolve.ts +76 -0
  74. package/src/cmd-jobs-spec.ts +21 -2
  75. package/src/cmd-jobs.ts +10 -6
  76. package/src/cmd-mail-spec.ts +28 -0
  77. package/src/cmd-mail.ts +70 -0
  78. package/src/cmd-mcp-lock.ts +69 -0
  79. package/src/cmd-mcp-spec.ts +23 -4
  80. package/src/cmd-mcp.ts +17 -3
  81. package/src/cmd-new-spec.ts +8 -1
  82. package/src/cmd-new.ts +61 -9
  83. package/src/cmd-planned.ts +0 -6
  84. package/src/cmd-repro-spec.ts +31 -0
  85. package/src/cmd-repro.ts +174 -0
  86. package/src/cmd-stop-spec.ts +21 -0
  87. package/src/cmd-stop.ts +166 -0
  88. package/src/cmd-test-spec.ts +17 -4
  89. package/src/cmd-test.ts +70 -14
  90. package/src/cmd-upgrade-spec.ts +26 -0
  91. package/src/cmd-upgrade.ts +274 -0
  92. package/src/cmd-verify-spec.ts +3 -3
  93. package/src/db-backfill.ts +6 -0
  94. package/src/db-diagnostics.ts +106 -0
  95. package/src/db-generate.ts +9 -0
  96. package/src/db-reset-template.ts +1 -1
  97. package/src/db-rolling.ts +45 -0
  98. package/src/db-seed-generate.ts +188 -0
  99. package/src/deploy-verify-findings.ts +98 -0
  100. package/src/deploy-verify-flags.ts +109 -0
  101. package/src/deploy-verify-grade.ts +185 -0
  102. package/src/deploy-verify-queues.ts +53 -0
  103. package/src/dev-boot.ts +35 -15
  104. package/src/dev-child-watch.ts +16 -4
  105. package/src/dev-dashboard-guard.ts +8 -1
  106. package/src/dev-dashboard.ts +7 -0
  107. package/src/dev-lock-identity.ts +61 -0
  108. package/src/dev-lock-port-holder.ts +80 -0
  109. package/src/dev-lock-steal.ts +124 -0
  110. package/src/dev-lock.ts +92 -120
  111. package/src/dev-mail-templates.ts +51 -0
  112. package/src/dev-rebuild.ts +26 -2
  113. package/src/dev-route-table.ts +17 -0
  114. package/src/dev-supervisor.ts +5 -2
  115. package/src/dev-watch-tree.ts +11 -10
  116. package/src/dev-watch.ts +1 -1
  117. package/src/dispatch.ts +29 -6
  118. package/src/docs-section.ts +69 -0
  119. package/src/docs-sources.ts +100 -0
  120. package/src/doctor-auth.ts +11 -2
  121. package/src/doctor-chart-alerts.ts +55 -0
  122. package/src/doctor-pooler.ts +107 -0
  123. package/src/doctor-rolling.ts +35 -0
  124. package/src/doctor-row-security.ts +40 -0
  125. package/src/document-options.ts +13 -0
  126. package/src/drill-sync-lane.ts +25 -0
  127. package/src/drill-targets.ts +39 -0
  128. package/src/duplicate-packages.ts +1 -1
  129. package/src/env-production.ts +93 -0
  130. package/src/error-codes-shot.ts +59 -0
  131. package/src/error-codes-tooling.ts +29 -0
  132. package/src/error-codes.ts +100 -54
  133. package/src/error-contract.ts +2 -2
  134. package/src/errors.ts +22 -3
  135. package/src/exercise-database.ts +99 -0
  136. package/src/exercise-findings.ts +159 -0
  137. package/src/exercise-overrides.ts +42 -0
  138. package/src/exercise-registration.ts +58 -0
  139. package/src/exercise-static.ts +88 -0
  140. package/src/exercise-subject.ts +69 -0
  141. package/src/failure-capture-boot.ts +30 -0
  142. package/src/framework-env.ts +7 -1
  143. package/src/framework-schema.ts +72 -5
  144. package/src/generate-catalog-locales.ts +19 -0
  145. package/src/generate-chart-alerts.ts +156 -0
  146. package/src/generate-feature-required.ts +65 -0
  147. package/src/generate-feature.ts +83 -6
  148. package/src/generate-files.ts +24 -0
  149. package/src/generate-format-written.ts +32 -0
  150. package/src/generate-kinds.ts +6 -0
  151. package/src/generate-sign-in-config.ts +100 -0
  152. package/src/generate-sign-in-manifest.ts +28 -0
  153. package/src/generate-sign-in-mode.ts +101 -0
  154. package/src/generate-sign-in.ts +250 -0
  155. package/src/generate-write.ts +44 -7
  156. package/src/hand-rolled-socket.ts +72 -0
  157. package/src/i18n-app-key-shapes.ts +29 -0
  158. package/src/i18n-audit.ts +10 -3
  159. package/src/i18n-registration.ts +18 -3
  160. package/src/index.ts +2 -0
  161. package/src/island-bundle.ts +4 -3
  162. package/src/island-disposer.ts +116 -0
  163. package/src/island-realtime-state.ts +34 -6
  164. package/src/island-realtime.ts +6 -2
  165. package/src/island-routes.ts +3 -2
  166. package/src/isr-query-advice.ts +6 -11
  167. package/src/isr-query-guard.ts +4 -3
  168. package/src/job-budget.ts +95 -0
  169. package/src/jobs-json.ts +11 -0
  170. package/src/lint-findings.ts +104 -0
  171. package/src/local-cli.ts +34 -4
  172. package/src/loopback-host.ts +2 -2
  173. package/src/mail-preview-findings.ts +24 -0
  174. package/src/mcp-errors.ts +82 -2
  175. package/src/mcp-host.ts +11 -1
  176. package/src/mcp-lock-findings.ts +57 -0
  177. package/src/measure-paths.ts +12 -4
  178. package/src/measure-scope.ts +27 -13
  179. package/src/messages-client.ts +23 -0
  180. package/src/messages-db.ts +56 -0
  181. package/src/messages-gate.ts +107 -0
  182. package/src/messages-repro.ts +11 -0
  183. package/src/messages-shot.ts +51 -0
  184. package/src/messages-upgrade.ts +45 -0
  185. package/src/messages.ts +53 -178
  186. package/src/metrics-endpoint.ts +23 -5
  187. package/src/new-shapes-generate.ts +71 -0
  188. package/src/new-shapes.ts +63 -0
  189. package/src/openapi-compile-ir.ts +68 -0
  190. package/src/openapi-compile-schema.ts +284 -0
  191. package/src/openapi-compile.ts +273 -0
  192. package/src/openapi-refuse.ts +45 -0
  193. package/src/otlp-export.ts +2 -1
  194. package/src/output-findings.ts +123 -0
  195. package/src/output.ts +32 -8
  196. package/src/package-boundaries.ts +110 -0
  197. package/src/page-pins.ts +23 -0
  198. package/src/plain-routes.ts +13 -0
  199. package/src/prerender-entries.ts +59 -0
  200. package/src/prerender-heaviest.ts +27 -0
  201. package/src/prerender-preload.ts +6 -0
  202. package/src/prerender-report.ts +12 -0
  203. package/src/prerender-seo-reads.ts +38 -0
  204. package/src/prerender-served.ts +22 -7
  205. package/src/prerender-vitals.ts +42 -0
  206. package/src/prerender.ts +61 -32
  207. package/src/pwa-artifacts.ts +4 -3
  208. package/src/pwa-manifest-locales.ts +1 -1
  209. package/src/registry.ts +18 -0
  210. package/src/repro-bundle.ts +65 -0
  211. package/src/repro-errors.ts +47 -0
  212. package/src/repro-job.ts +72 -0
  213. package/src/repro-literal.ts +41 -0
  214. package/src/repro-request.ts +48 -0
  215. package/src/repro-write.ts +100 -0
  216. package/src/retain-sweep.ts +27 -0
  217. package/src/retained-assets-boot.ts +113 -0
  218. package/src/retained-assets-layout.ts +110 -0
  219. package/src/retained-assets-read.ts +142 -0
  220. package/src/retained-assets-sweep.ts +70 -0
  221. package/src/retained-assets.ts +171 -0
  222. package/src/revalidated-response.ts +8 -5
  223. package/src/role-replicator.ts +7 -1
  224. package/src/role-start-devices.ts +119 -0
  225. package/src/role-start-types.ts +6 -0
  226. package/src/role-start.ts +22 -5
  227. package/src/role-sync.ts +7 -1
  228. package/src/route-islands.ts +42 -0
  229. package/src/route-table.ts +2 -1
  230. package/src/row-security-boot.ts +58 -0
  231. package/src/row-security-wanted.ts +32 -0
  232. package/src/runtime-admin.ts +0 -1
  233. package/src/runtime-assets-site-cache.ts +55 -0
  234. package/src/runtime-assets-site.ts +136 -0
  235. package/src/runtime-assets.ts +99 -55
  236. package/src/runtime-bindings.ts +19 -3
  237. package/src/runtime-cache.ts +23 -14
  238. package/src/runtime-isr-outcome.ts +114 -60
  239. package/src/runtime-purge.ts +14 -2
  240. package/src/runtime-queue.ts +31 -4
  241. package/src/runtime-render-document.ts +204 -0
  242. package/src/runtime-render.ts +48 -198
  243. package/src/runtime-replica-position.ts +62 -0
  244. package/src/runtime-replica.ts +18 -3
  245. package/src/runtime-s3-path-style.ts +17 -0
  246. package/src/runtime-services.ts +19 -6
  247. package/src/runtime-statement-bound.ts +22 -0
  248. package/src/runtime-storage-upload.ts +9 -0
  249. package/src/runtime-storage.ts +41 -81
  250. package/src/schema-diff-views.ts +54 -0
  251. package/src/schema-diff.ts +21 -3
  252. package/src/schema-drift.ts +28 -9
  253. package/src/schema-dump-drift.ts +91 -7
  254. package/src/script-csp.ts +1 -1
  255. package/src/seo-cards.ts +26 -0
  256. package/src/seo-routes.ts +22 -8
  257. package/src/serve-boot.ts +54 -21
  258. package/src/serve-drain.ts +1 -0
  259. package/src/serve-env.ts +26 -8
  260. package/src/serve-retire.ts +12 -6
  261. package/src/serve-web.ts +42 -4
  262. package/src/serve.ts +9 -7
  263. package/src/site-asset-routes.ts +7 -1
  264. package/src/site-config.ts +4 -0
  265. package/src/site-llms-pages.ts +106 -0
  266. package/src/site-llms.ts +64 -0
  267. package/src/site-seo-write.ts +4 -2
  268. package/src/site-seo.ts +55 -6
  269. package/src/source-mtime.ts +50 -0
  270. package/src/static-capture-errors.ts +41 -0
  271. package/src/static-capture.ts +56 -0
  272. package/src/static-cards-serve.ts +39 -0
  273. package/src/static-cards.ts +93 -0
  274. package/src/static-document.ts +14 -13
  275. package/src/static-images.ts +115 -0
  276. package/src/stream-hole-scope.ts +55 -0
  277. package/src/style-bundle.ts +1 -1
  278. package/src/style-routes.ts +3 -2
  279. package/src/sw-precache-plan.ts +2 -2
  280. package/src/sw-routes.ts +2 -2
  281. package/src/templates/action-tests.ts +306 -0
  282. package/src/templates/action.ts +81 -269
  283. package/src/templates/backfill.ts +1 -1
  284. package/src/templates/entity.ts +21 -5
  285. package/src/templates/github/ci.yml.ts +8 -9
  286. package/src/templates/guard-bare-error.ts +2 -2
  287. package/src/templates/guard-error-declared.ts +263 -0
  288. package/src/templates/guard-native-confirm.ts +132 -0
  289. package/src/templates/guard-raw-fetch.ts +151 -0
  290. package/src/templates/guard-raw-interval.ts +121 -0
  291. package/src/templates/guard-repo-raw-sql.ts +71 -2
  292. package/src/templates/job.ts +1 -1
  293. package/src/templates/naming.ts +5 -0
  294. package/src/templates/pinned/docker-compose.yml +6 -0
  295. package/src/templates/pinned-images.ts +8 -0
  296. package/src/templates/policy.ts +85 -2
  297. package/src/templates/query.ts +70 -13
  298. package/src/templates/resource-create.ts +3 -1
  299. package/src/templates/resource-form-island.ts +15 -4
  300. package/src/templates/resource-page-db.ts +83 -0
  301. package/src/templates/resource-page.ts +161 -52
  302. package/src/templates/route.ts +4 -6
  303. package/src/templates/scaffold-app.ts +3 -0
  304. package/src/templates/scaffold-auth.ts +2 -1
  305. package/src/templates/scaffold-bin.ts +5 -52
  306. package/src/templates/scaffold-claude-agents.ts +19 -19
  307. package/src/templates/scaffold-claude-commands.ts +28 -31
  308. package/src/templates/scaffold-claude.ts +26 -23
  309. package/src/templates/scaffold-container-compose.ts +6 -1
  310. package/src/templates/scaffold-container.ts +24 -4
  311. package/src/templates/scaffold-dashboard-bare.ts +3 -3
  312. package/src/templates/scaffold-dashboard-example-tests.ts +49 -9
  313. package/src/templates/scaffold-dashboard-example-view.ts +156 -0
  314. package/src/templates/scaffold-dashboard-example.ts +43 -131
  315. package/src/templates/scaffold-dashboard-shared.ts +3 -7
  316. package/src/templates/scaffold-db-client.ts +20 -6
  317. package/src/templates/scaffold-db-package.ts +5 -14
  318. package/src/templates/scaffold-docs.ts +73 -57
  319. package/src/templates/scaffold-domain-package.ts +18 -14
  320. package/src/templates/scaffold-entries.ts +7 -2
  321. package/src/templates/scaffold-exercise.ts +37 -0
  322. package/src/templates/scaffold-guards.ts +13 -1
  323. package/src/templates/scaffold-helm-alerts.ts +234 -0
  324. package/src/templates/scaffold-helm-templates.ts +7 -5
  325. package/src/templates/scaffold-helm.ts +46 -12
  326. package/src/templates/scaffold-i18n.ts +6 -2
  327. package/src/templates/scaffold-mcp-endpoint.ts +182 -0
  328. package/src/templates/scaffold-mcp-json.ts +31 -0
  329. package/src/templates/scaffold-mcp-package.ts +59 -56
  330. package/src/templates/scaffold-repo.ts +7 -2
  331. package/src/templates/scaffold-shell.ts +45 -79
  332. package/src/templates/scaffold-test-services.ts +13 -5
  333. package/src/templates/shapes/api-keys.ts +170 -0
  334. package/src/templates/shapes/api.ts +101 -0
  335. package/src/templates/shapes/blog-site.ts +340 -0
  336. package/src/templates/shapes/blog-slice.ts +265 -0
  337. package/src/templates/shapes/blog.ts +114 -0
  338. package/src/templates/shapes/dashboard-detail.ts +242 -0
  339. package/src/templates/shapes/dashboard.ts +53 -0
  340. package/src/templates/shapes/index.ts +17 -0
  341. package/src/templates/shapes/overlay.ts +99 -0
  342. package/src/templates/shapes/shared-edits.ts +46 -0
  343. package/src/templates/sign-in-action-tests.ts +237 -0
  344. package/src/templates/sign-in-authenticator.ts +112 -0
  345. package/src/templates/sign-in-oidc.ts +189 -0
  346. package/src/templates/sign-in-pages.ts +148 -0
  347. package/src/templates/sign-in-password.ts +216 -0
  348. package/src/templates/sign-in-shared.ts +268 -0
  349. package/src/templates/slice-foundation.ts +70 -15
  350. package/src/test-batches.ts +1 -1
  351. package/src/test-database.ts +1 -1
  352. package/src/test-shards.ts +7 -1
  353. package/src/test-workers.ts +18 -12
  354. package/src/tool-gate-names.ts +34 -0
  355. package/src/upgrade-agents-link.ts +49 -0
  356. package/src/upgrade-bun-floor.ts +225 -0
  357. package/src/upgrade-gate.ts +228 -0
  358. package/src/upgrade-pins.ts +63 -0
  359. package/src/upgrade-plan.ts +214 -0
  360. package/src/upgrade-registry.ts +53 -0
  361. package/src/upgrade-report.ts +139 -0
  362. package/src/upgrade-root.ts +74 -0
  363. package/src/verify-budgets-build.ts +118 -0
  364. package/src/verify-checks.ts +67 -24
  365. package/src/verify-coverage-run.ts +1 -1
  366. package/src/verify-e2e-lane.ts +41 -0
  367. package/src/verify-exercise.ts +64 -0
  368. package/src/verify-floor.ts +41 -0
  369. package/src/verify-merge.ts +19 -4
  370. package/src/verify-role-load.ts +2 -0
  371. package/src/verify-run.ts +28 -8
  372. package/src/verify-shard.ts +12 -2
  373. package/src/verify-skip-ceiling.ts +87 -0
  374. package/src/verify-step.ts +5 -0
  375. package/src/verify-tests.ts +25 -5
  376. package/src/view-drift.ts +55 -0
  377. package/src/worker-bundle.ts +3 -2
  378. package/src/workspace-graph.ts +38 -19
  379. package/src/write-generated.ts +43 -0
  380. package/src/zone-boot.ts +20 -0
  381. package/wiki/Actions.md +342 -0
  382. package/wiki/Admin-Dashboard.md +432 -0
  383. package/wiki/Agents.md +958 -0
  384. package/wiki/Auth.md +195 -0
  385. package/wiki/Bare-VM.md +111 -0
  386. package/wiki/Batching-And-Preloading.md +136 -0
  387. package/wiki/Building-Your-Own-Base.md +203 -0
  388. package/wiki/CI-Parallel-Gate.md +220 -0
  389. package/wiki/CLI-Reference.md +1894 -0
  390. package/wiki/Caching-And-Invalidation.md +255 -0
  391. package/wiki/Client-Data.md +194 -0
  392. package/wiki/Client-Navigation.md +513 -0
  393. package/wiki/Compared.md +93 -0
  394. package/wiki/Configuration.md +868 -0
  395. package/wiki/Contributing.md +12 -0
  396. package/wiki/Deploy-Recipes.md +122 -0
  397. package/wiki/Deployment.md +508 -0
  398. package/wiki/Entities-And-Migrations.md +1116 -0
  399. package/wiki/FAQ.md +175 -0
  400. package/wiki/Feature-Flags.md +65 -0
  401. package/wiki/Getting-Started.md +221 -0
  402. package/wiki/Home.md +160 -0
  403. package/wiki/I18n.md +239 -0
  404. package/wiki/Installation.md +206 -0
  405. package/wiki/Interface-Rules.md +339 -0
  406. package/wiki/Jobs-And-Workflows.md +783 -0
  407. package/wiki/Known-Gaps.md +185 -0
  408. package/wiki/MCP-And-AI.md +753 -0
  409. package/wiki/Mail.md +138 -0
  410. package/wiki/Migrating-An-Existing-App.md +594 -0
  411. package/wiki/Migrating-From-Django.md +54 -0
  412. package/wiki/Migrating-From-Laravel.md +53 -0
  413. package/wiki/Migrating-From-Node-Drizzle.md +53 -0
  414. package/wiki/Migrating-From-Rails.md +53 -0
  415. package/wiki/Migrations-And-Backfills.md +552 -0
  416. package/wiki/Money.md +338 -0
  417. package/wiki/N-Plus-One-Detection.md +197 -0
  418. package/wiki/Notify.md +208 -0
  419. package/wiki/Observability.md +245 -0
  420. package/wiki/Outbound-HTTP.md +109 -0
  421. package/wiki/PWA-And-Offline.md +343 -0
  422. package/wiki/Policies-And-Authz.md +251 -0
  423. package/wiki/Project-Layout.md +150 -0
  424. package/wiki/Queries-And-Live-Queries.md +429 -0
  425. package/wiki/Realtime.md +734 -0
  426. package/wiki/Resource-Management.md +121 -0
  427. package/wiki/Routes-And-Render-Modes.md +521 -0
  428. package/wiki/SEO.md +288 -0
  429. package/wiki/Scheduled-Tasks.md +175 -0
  430. package/wiki/Scraping.md +493 -0
  431. package/wiki/Shapes.md +63 -0
  432. package/wiki/Stability.md +97 -0
  433. package/wiki/Static-Assets.md +166 -0
  434. package/wiki/Storage-And-Uploads.md +404 -0
  435. package/wiki/Testing.md +1314 -0
  436. package/wiki/The-Eight-Primitives.md +247 -0
  437. package/wiki/Theming.md +362 -0
  438. package/wiki/Timezones-And-Dates.md +194 -0
  439. package/wiki/Troubleshooting.md +163 -0
  440. package/wiki/Tutorial-01-First-App.md +289 -0
  441. package/wiki/Tutorial-02-First-Feature.md +339 -0
  442. package/wiki/Tutorial-03-Auth-And-Admin.md +379 -0
  443. package/wiki/Tutorial-04-Jobs-And-Realtime.md +263 -0
  444. package/wiki/Tutorial-05-Deploy-Free.md +231 -0
  445. package/wiki/Tutorial-06-Growing-Up.md +158 -0
  446. package/wiki/UI-Components.md +156 -0
  447. package/wiki/Upgrading.md +3667 -0
  448. package/src/stored-object-headers.ts +0 -43
@@ -0,0 +1,255 @@
1
+ # Caching and invalidation
2
+
3
+ Four tiers, one invalidation graph. You declare what a write touches; the framework decides what to evict.
4
+
5
+ `As of 2026-08`. Stable API — semver from here ([Upgrading](Upgrading)).
6
+
7
+ ## The four tiers
8
+
9
+ | Tier | Store | Lifetime | Hit cost | Use |
10
+ |---|---|---|---|---|
11
+ | 1 | **Request memo** | one request (AsyncLocalStorage) | ~0 | the same `query` called by three components resolves once |
12
+ | 2 | **In-process LRU** | process lifetime, size-bounded | ~microseconds | hot rows, policy lookups, compiled templates; per-instance, so treat as a probabilistic hit |
13
+ | 3 | **Redis** (`redisClient()`, `@ultimat3/core/redis`) | cross-instance, TTL + tag sets | ~1ms | shared query results, rendered fragments, session-adjacent data |
14
+ | 4 | **CDN / HTTP headers** | client + edge | `Cache-Control`, `ETag`, `stale-while-revalidate` | static + ISR pages, images, assets |
15
+
16
+ Read order is **1 → 2 → 3 → origin**. A tier is never consulted for a request whose `policy` has not already passed.
17
+
18
+ **A cached value is JSON + `Date` + `bigint` + `Map` + `Set`, and every tier answers the same shape.** `As of 2026-10`, tiers 1–3 store one encoding (`@ultimat3/cache`'s `value-codec.ts`) and decode a fresh copy per hit: a `Date` is a `Date` whether the memo, the LRU or Redis answered, and mutating a hit never reaches the next reader. A class instance comes back as a plain object; a cycle is `X_CACHE_VALUE_UNENCODABLE` at the write.
19
+
20
+ **A cache key is framework-generated, never hand-built.** `As of 2026-11` it is the query name, the read's **authority**, a fingerprint of the parsed input and the read's sorted tag keys — `cacheKeyFor` in `@ultimat3/query`. The authority is `cache: { scope }`: `actor` by default (one key per caller — the narrowest, always correct), `tenant` (one per org; an org-less actor narrows back to itself) or `global`. Widening is a written statement that every member of the org, or everyone, sees the same rows. Policy still runs on every read before a tier is consulted; tier 1 is keyed by `Ctx` identity.
21
+
22
+ **A null holds for its own lease.** `query({ single: true, cache: { ttlMs, negativeTtlMs } })`: a `single` read that matched no row is stored as `null` and kept `negativeTtlMs`, so a lookup that missed 40 ms before its row landed says "does not exist" for that long and not for the whole `ttlMs`. Only `single: true` answers `null` — an empty list is a list and keeps `ttlMs`. Non-positive or non-finite is `X_QUERY_CACHE_TTL_INVALID` at `query()`, the tiers' own rule. `As of 2026-11`
23
+
24
+ **A read may answer 304.** `query({ etag: true })` adds tier 4 to a read: `private, no-cache`, a strong `ETag`, and a bodiless 304 for a held validator — after the policy and the rate limit ([Queries](Queries-And-Live-Queries#fields)).
25
+
26
+ | Tier | Opt-out / requirement |
27
+ |---|---|
28
+ | 1 | always on; no configuration |
29
+ | 2 | optional per entry — `local: false` for large or per-tenant-unbounded values |
30
+ | 3 | **required** for any entry an ISR page depends on, because regeneration happens on a different instance than the write |
31
+ | 4 | emitted from the route's render mode; never hand-set on a response |
32
+
33
+ ## Entity tags are the invalidation graph
34
+
35
+ A tag is a typed handle derived from an [entity](Entities-And-Migrations). There is no string-keyed invalidation API.
36
+
37
+ ```ts
38
+ export const tag = tags({
39
+ post: entityTag(posts), // tag.post, tag.post.id(x)
40
+ feed: derivedTag('feed', [tags.post]), // invalidating post cascades to feed
41
+ });
42
+ ```
43
+
44
+ | Tag form | Scope | Example |
45
+ |---|---|---|
46
+ | `tag.post` | all posts | a schema-level change |
47
+ | `tag.post.id(postId)` | one row | the common case; narrowest eviction |
48
+ | `tag.feed` | derived; declares `[tag.post]` as an upstream | list views that a post membership affects |
49
+
50
+ | Declared where | By |
51
+ |---|---|
52
+ | Queries | **automatic** — acquired from the tables their `sql` touches |
53
+ | Routes | `revalidate: { tags: [...] }` |
54
+ | Actions / mutators | `cache: { invalidates: [...] }` |
55
+ | LLM calls | `cache: { invalidates: [...] }` |
56
+
57
+ The graph is a build-time artifact in `x.manifest.json`, so it records exactly what a write will evict — before you run it. `x cache graph --json` is the planned reader of it; today it is `x dev` → the `/_x` cache panel. Tag typing comes from a generated registry augmentation; `x manifest` regenerates it.
58
+
59
+ ## One hop, all tiers
60
+
61
+ ```ts
62
+ // action
63
+ export const publishPost = action({
64
+ input: t.object({ postId: t.uuid, notify: t.boolean.default(true) }),
65
+ output: PostView,
66
+ policy: can('post:publish', ({ input, actor }) => ownsPost(actor, input.postId)),
67
+ cache: { invalidates: [tag.post, tag.feed] },
68
+ mcp: { expose: true, description: 'Publish a draft post' },
69
+ async handle({ input, ctx }) {
70
+ const post = await ctx.posts.publish(input.postId);
71
+ if (input.notify) await notifySubscribers.enqueue({ postId: post.id, orgId: post.orgId });
72
+ return post;
73
+ },
74
+ });
75
+ ```
76
+
77
+ `invalidates: [tag.post, tag.feed]` fans out on commit:
78
+
79
+ | Target | Mechanism | Timing |
80
+ |---|---|---|
81
+ | Tier 1 request memo | drop entries carrying the tag | immediate, same request |
82
+ | Tier 2 in-process LRU (**all instances**) | tag-invalidation message on NATS | ~ms, best-effort; a missed message costs a stale read until TTL, never a wrong write |
83
+ | Tier 3 Redis | `SREM`/`DEL` over the tag's key set | immediate, inside the fan-out |
84
+ | ISR pages | routes whose `revalidate.tags` include the tag are marked stale → regenerated in background | next request serves stale, regen enqueued as a job |
85
+ | CDN | purge by surrogate key through the configured `PurgeDriver` — a row bust purges `post:1` and `post`, a collection bust `e:post` and `post` | seconds; `stale-while-revalidate` covers the gap |
86
+ | Live queries | the same commit already flows through logical replication | **independent path** — realtime does not depend on cache invalidation |
87
+
88
+ Fan-out runs **after the handler resolves, in the same call** — `bustAfterCommit` awaits `invalidateTags()` directly (`packages/action/src/cache-gate.ts`), never through the outbox `As of 2026-08`. A handler that throws never reaches it, so a rolled-back write never purges; a process that dies between the commit and the fan-out leaves those entries until their TTL. An action invoked **inside `withTransaction`** — an app's own unit of work, or another action's — hands the fan-out to the root transaction's `onCommit` instead `As of 2026-10-02`: it runs once the root `COMMIT` is answered and never for a rollback, so a concurrent read cannot refill an entry from rows that are not yet durable.
89
+
90
+ > **Tier 3 invalidation is slot-local as of 2.0.0, and was not through 1.2.0.** The Lua script `DEL`ed keys it never declared in `KEYS`, which single-node Redis tolerates and **Dragonfly and Redis Cluster reject** — a cluster cannot route a key it was not told about. The script now returns the member list and the tier deletes the value keys client-side, one key per `DEL`, so every delete is slot-local. On 1.2.0 and earlier: use single-node Redis, or drop the shared tier → [Known gaps](Known-Gaps).
91
+
92
+ There is exactly one fan-out entry point in the implementation (`invalidateTags()`); no caller reaches a tier directly. Tier failures are collected into an invalidation report — **a cache tier may never fail a business write.**
93
+
94
+ The write side holds the same rule one layer up: a fan-out that refuses outright — a tag no entity declared, `X_CACHE_TAG_UNKNOWN` — is absorbed rather than raised: one `action.invalidate.failed` error line, entries live until their TTL, and the caller keeps the write it already made. A replayed idempotent call (`idempotent: true` + a repeated `Idempotency-Key`) busts nothing at all: no handler ran, and the first call already did.
95
+
96
+ **N concurrent misses on one key are ONE `load()`.** The stack holds a single-flight map, per stack
97
+ rather than per module — two stacks are two ladders and must not join each other's loads. A `load()`
98
+ may hold its key for `loadDeadlineMs`, default **30 s**, after which a later reader is allowed to
99
+ start its own instead of joining a promise that may never resolve. The deadline frees the **key**
100
+ and nothing else: `load()` is your function and the stack holds no signal that could abort it, so
101
+ the wedged load runs on and the readers already holding its promise still get whatever it answers.
102
+ Worst case is one duplicate fill, which the ladder's last-write-wins `set` already tolerates —
103
+ against a key pinned for the life of the process. 30 s because `@ultimat3/http` abandons the request
104
+ waiting on that read at the same bound; a load still running past it has no reader left to serve.
105
+ `As of 2026-08-23`. Set it per stack: `cacheStack(tiers, { loadDeadlineMs })`.
106
+
107
+ **Who shares that load, and who does not** `As of 2026-11`. A read joins it unless the load's fence was
108
+ moved: after a `drop`, a `write` or a tag bust covering the key or the reader's own tags, the next
109
+ read leads a fresh load, so a read-your-write never answers from data loaded before the write. A
110
+ reader that joined before the bust keeps the load it joined. And a load whose leader was aborted
111
+ (`ctx.signal`, a closed tab) does not fail the readers who joined it: the first one with a live
112
+ signal loads again under its own context and the rest join that. A load that fails with its leader
113
+ still live fails its joiners too.
114
+
115
+ **Redis down is a slow miss, never a hang** `As of 2026-11`. A paused server accepts a command and never answers it; unbounded, that hung a read 45 s and a bust 20 s on an action's commit path.
116
+
117
+ | Redis is | A read | A bust (`invalidateTags`) |
118
+ |---|---|---|
119
+ | up | answers in milliseconds | answers |
120
+ | paused | one command misses `redisTier({ commandTimeoutMs })` (default 150 ms) → a miss, the source answers; the value path parks 5 s, every read there an immediate miss with nothing sent | its own 1,000 ms deadline, never parked — a skipped bust is stale rows until TTL; past it, `report.errors` |
121
+ | stopped | refused at once → a miss | refused at once → `report.errors`; the same tags again once it answers |
122
+ | restarted | every pod reconnects on its own, no restart | answers |
123
+
124
+ A missed deadline or a park is `X_CACHE_DRIVER_UNAVAILABLE` in `recentTierFailures()`; a stopped server's refusal is recorded with Bun's `ERR_REDIS_CONNECTION_CLOSED` message and no `code`. While the store is gone each pod still collapses its own concurrent misses to one `load()`; across pods, Postgres takes one load per pod per key — the rung's known cost. The jobs queue on the same client refuses an enqueue with `X_DRIVER_UNAVAILABLE` instead of waiting ([Jobs](Jobs-And-Workflows)).
125
+
126
+ The read ladder keeps the same rule with no report to hand back: a tier that throws on `get`, `set` or `del` is a tier that did not answer, so the walk continues and the source is still returned. A value too large for the in-process LRU (`X_CACHE_TOO_LARGE`) costs the entry, never the read. Every absorbed refusal lands in a bounded log — `recentTierFailures()`, last 100, newest first, carrying the tier, the operation, the key and the `X_*` code — plus one `cache.tier.failed` warn. The one call left to throw is the load itself: it *is* the business read.
127
+
128
+ ## Failure modes removed
129
+
130
+ The bug is never "the cache is wrong". The bug is that invalidation is a *decision made at a distance*: the developer editing `publishPost` must remember which of nine cached things this write affects, including two added last month by someone else.
131
+
132
+ | Failure mode | Cause | Removed by |
133
+ |---|---|---|
134
+ | Stale page after publish | forgot one `revalidate` call | tags are declared on the route, resolved from the graph |
135
+ | Purging too much | uncertainty → `flushAll` | narrow `tag.post.id(x)` is the ergonomic default |
136
+ | Tier drift | Redis purged, CDN not | one fanout, all tiers |
137
+ | Leak across tenants | hand-built cache key missing the tenant | keys are framework-generated from the query name, the read's authority (`cache: { scope }`, default `actor`), its parsed input and its tags |
138
+ | Stale forever | a query whose tables no tag covers | the tag rule — **not yet a gate**: `X_CACHE_UNTAGGED_QUERY` is reserved `As of 2026-08` and nothing raises it, so a cached query no tag covers is cached and never invalidated ([Error codes → Reserved codes](Error-Codes#reserved-codes)) |
139
+ | Silent typo | `invalidates: [tag.pots]` | a compile error against the generated registry; at runtime `X_CACHE_TAG_UNKNOWN`, which a write logs as `action.invalidate.failed` rather than failing the commit it followed |
140
+
141
+ Agents are measurably bad at *distant* invariants — "edit here, remember to also edit there" is where LLM-written code regresses most. Declaring `invalidates` at the write site is local, checkable, and typed.
142
+
143
+ ## The CDN leg
144
+
145
+ The CDN is the one tier Ultimate never reads back from, so the emitted header and the purge call
146
+ are the whole contract. An edge purges by exact key, so the two-way rule every other tier keeps —
147
+ a collection bust reaches its rows, a row bust reaches the lists that held it — is built from two
148
+ key lists, `As of 2026-10`:
149
+
150
+ | | Row tag `post:1` | Collection tag `post` |
151
+ |---|---|---|
152
+ | a response carries (`cacheHeaders()`, `surrogateKeys()`) | `post:1` `e:post` | `post` `e:post` |
153
+ | a bust purges | `post:1` `post` | `e:post` `post` |
154
+
155
+ `e:<entity>` is the entity index: `len(entity) + 3` bytes per distinct entity in each header (7
156
+ for `post`, 14 across both). A
157
+ key carrying whitespace or a comma is refused at emission (`X_CACHE_PURGE_FAILED`), not on the
158
+ purge — a CDN splits it into keys nothing ever names.
159
+
160
+ Which responses carry keys, `As of 2026-10-02`:
161
+
162
+ | Response | `Surrogate-Key` / `Cache-Tag` |
163
+ |---|---|
164
+ | an `isr` document whose route declares `revalidate.tags`, answered as a shared (`public`) response | yes — those tags through `surrogateKeys()`, on a miss, a hit and a served-stale answer |
165
+ | an `isr` document with `revalidate.ttl` only | no — nothing purges it; `s-maxage` is its whole lease |
166
+ | the same `isr` document answered to a **signed-in** visitor | no — the `cache-headers` stage rewrites it to `private` and strips both headers |
167
+ | `static`, `ssr`, `stream` documents; assets; action and query responses | no — none is a tagged shared response (`ssr`'s `cache` takes no `tags`; actions and queries are `no-store`) |
168
+ | a response your own handler tags (`ctx.cache = { mode: 'public', tags }`, or `cacheHeaders()`) | whatever you pass — hand `CacheHint.tags` the output of `surrogateKeys(tags)`, which `@ultimat3/http` emits verbatim |
169
+
170
+ Before this date **no shipped response carried a key at all**: the purge drivers were called with
171
+ keys nothing at the edge had ever been tagged with, and answered success. An unpurgeable tag in
172
+ `revalidate.tags` is refused when the route registers (`X_ROUTE_MODE_INVALID`, naming the file).
173
+
174
+ | Driver | Purge | Purge all | Per call |
175
+ |---|---|---|---|
176
+ | `noopPurgeDriver()` | echoes the keys back | resolves | — |
177
+ | `fastlyPurgeDriver({ apiToken, serviceId })` | `POST /service/<id>/purge` with `surrogate_keys` | `POST /service/<id>/purge_all` | 256 keys |
178
+ | `cloudflarePurgeDriver({ apiToken, zoneId })` | `POST /zones/<id>/purge_cache` with `tags` | same call with `purge_everything` | 30 tags |
179
+
180
+ Which one a process installs is decided from the environment — `FASTLY_API_TOKEN` +
181
+ `FASTLY_SERVICE_ID`, or `CLOUDFLARE_API_TOKEN` + `CLOUDFLARE_ZONE_ID`. See
182
+ [Configuration → CDN purge](Configuration#cdn-purge). With neither pair set, nothing is purged and
183
+ no `cdn` line appears in the invalidation report: a tier that reported keys an edge that does not
184
+ exist had accepted would be worse than no tier at all.
185
+
186
+ A refusal is `X_CACHE_PURGE_FAILED` with `meta.retryable`, collected into `report.errors` — a dead
187
+ CDN never fails the write that triggered the bust, and the entry expires by TTL instead.
188
+
189
+ ## Semantic cache for LLM calls
190
+
191
+ Model calls are slow and metered; exact-match caching almost never hits because prompts differ by a word.
192
+
193
+ ```ts
194
+ export const summarize = llm({
195
+ model: 'claude-sonnet-5-5',
196
+ cache: {
197
+ semantic: { threshold: 0.97, ttl: '7d' }, // partitions on the ACTOR by default
198
+ invalidates: [tag.post],
199
+ },
200
+ prompt: summarizePrompt, // versioned artifact, see 09-ai-first.md
201
+ });
202
+ ```
203
+
204
+ | Aspect | Rule |
205
+ |---|---|
206
+ | Store | pgvector table, one row per (prompt version, embedded input, scope) |
207
+ | Key | embedding of the rendered prompt + model id + **prompt version** + **request locale** |
208
+ | Hit | cosine similarity >= `threshold`; default 0.97, never below 0.9 |
209
+ | Scope | optional — the calling **actor** by default, so one org never reads another's completion. Widening is written down: `scope: () => 'global'` |
210
+ | Bypass | `temperature > 0` results are cached but flagged; `cache: false` for anything user-visible-and-unique |
211
+ | Invalidation | participates in the same tag graph; bumping the prompt version invalidates wholesale |
212
+ | Metrics | hit rate, tokens saved, cost saved — in `/_x` and `x ai cache --json` |
213
+
214
+ **The locale is part of the key, and not part of the scope** — `As of 2026-08-24`. A prompt that takes `locale` as a var differs by one token between languages while carrying a whole document, so the two rendered prompts are near-identical to an embedder: measured over the reference app's summarize template, **0.9986**, against the `threshold: 0.97` in the snippet above. Before this, whichever language asked first was served to everyone — an English reader got the Spanish summary, and the model had done nothing wrong.
215
+
216
+ No threshold fixes that, because the same number has to keep an honest repeat *above* it. So the locale sits in the unconditional half of the store key, beside the prompt version: a `scope` answers *who may share this answer*, and a locale is part of what the answer **is**. A written-down `scope: () => 'global'` is still partitioned by locale — declaring a shared store says your callers may read one another's summaries, never that a Spanish one will do for an English reader. Pages key by locale for the same reason ([ISR](Routes-And-Render-Modes)).
217
+
218
+ Also cached exactly (tier 3, not semantic): embeddings themselves, keyed by content hash + model. Re-embedding unchanged text is pure waste. See [MCP and AI](MCP-And-AI).
219
+
220
+ ## CLI — `x cache` is **planned**
221
+
222
+ Every row below exits `X_NOT_IMPLEMENTED` `As of 2026-08`, with `x dev` → the `/_x` cache panel as its `fix:`. A planned command declares no flags either, so `x cache graph --tag post` dies at the parser with `X_CLI_BAD_FLAG` before the honest message — call the bare form.
223
+
224
+ | Command | Will do |
225
+ |---|---|
226
+ | `x cache graph --json` | print the tag → dependents graph: cache keys, ISR routes, CDN paths, live queries. Build-time truth, no runtime call |
227
+ | `x cache graph --tag post --json` | the blast radius of one tag |
228
+ | `x cache bust <tag>` | run the real fanout for one tag, all tiers, and print the invalidation report |
229
+ | `x cache clear` | **dev only.** The only `flushAll` there would be; there is no runtime API for it |
230
+ | `x cache stats --json` | per-tier hit rate, byte usage against budget, evictions |
231
+
232
+ Today: `x dev` then the `/_x` cache panel, and `invalidateTags([...])` from `@ultimat3/cache` for a bust from code.
233
+
234
+ ## Errors
235
+
236
+ | Code | Cause | Fix |
237
+ |---|---|---|
238
+ | `X_CACHE_UNTAGGED_QUERY` | **reserved, nothing raises it** `As of 2026-08` — a query's tables are covered by no tag, so it could never be invalidated ([Error codes → Reserved codes](Error-Codes#reserved-codes)) | declare the entity tag, then `x manifest` |
239
+ | `X_CACHE_TAG_UNKNOWN` | `tag "<name>" is not declared by any entity` | `x manifest` |
240
+ | `X_CACHE_TOO_LARGE` | `entry "<key>" is <n>B, over the <tier> budget of <m>B` | `raise cache.<tier>.maxBytes in app.config.ts, or cache a projection instead of the row` |
241
+ | `X_CACHE_DRIVER_UNAVAILABLE` | `cache tier "<driver>" is unavailable` — a Redis command missed its deadline, or the tier is parked after one; or a purge driver built without its token | the error carries the exact config or command to fix |
242
+ | `X_CACHE_PURGE_FAILED` | `<driver> refused the purge (HTTP <status>)` — a wrong token, a zone without tag purge, a throttle, or a key a CDN would split | `meta.retryable === true` → the identical purge can land again; otherwise set the env key the `fix` names |
243
+
244
+ Verbatim shapes: [`packages/cache/src/errors.ts`](https://github.com/developerz-ai/ultimate/blob/main/packages/cache/src/errors.ts). Full index: [Error codes](Error-Codes).
245
+
246
+ ## Rules
247
+
248
+ - Cache keys are framework-generated. A hand-built key is a rejected PR.
249
+ - Every cached `query` carries at least one tag. Review catches it `As of 2026-08`, not the gate — `X_CACHE_UNTAGGED_QUERY` is reserved and no `x verify` step reads a query's tags.
250
+ - Never cache a value whose policy scope is not in its key.
251
+ - `flushAll` has no shipped caller: `x cache clear` is planned, and there is no runtime API for it.
252
+ - Cache misses must be correct and merely slower — no code path may depend on a hit.
253
+ - Prefer `tag.post.id(x)` over `tag.post`. Narrow eviction is the default, not an optimization.
254
+ - A cache tier may never fail a business read or write. Tier errors land in the invalidation report, or in `recentTierFailures()` on the read ladder.
255
+ - Realtime is not a cache tier. Live queries flow from logical replication on their own path ([Realtime](Realtime)).
@@ -0,0 +1,194 @@
1
+ # Client data
2
+
3
+ **Every browser request goes through one function, and every entity row it returns lands in one
4
+ record per `entity:id`.** You never write `fetch`, never write a store, and never mirror a row
5
+ into a signal by hand. Declare the entity, return its row, call the typed client.
6
+
7
+ Shipped in 21.0.0, both halves: the HTTP seam and the store that receives its records, with the
8
+ hooks that read it. The store is installed the first time a realtime hook runs on the page; records
9
+ answered before that wait on the page handle until it is. The design is
10
+ [`docs/architecture/21-client-data-layer.md`](https://github.com/developerz-ai/ultimate/blob/main/docs/architecture/21-client-data-layer.md).
11
+
12
+ ## One idiom per task
13
+
14
+ | Task | Write | Status |
15
+ |---|---|---|
16
+ | declare what a record is | `entity('post', { columns: { id: uuid().primaryKey(), … } })` | shipped |
17
+ | an action that returns a record | `output: Post.$schema`, or a schema that contains it | shipped in 21.0.0 |
18
+ | a query whose rows are records | `rows: Post.$schema` on `query()` | shipped in 21.0.0 |
19
+ | call an action from an island | `rpc<Api['actions']>({ baseUrl: '' })` | shipped; now goes through the one transport |
20
+ | call a query from an island | `queryClient<Api['queries']>({ baseUrl: '' })` | shipped; now goes through the one transport |
21
+ | ignore an answer for a principal that signed out | `if (isSuperseded(error)) return;` | shipped in 21.0.0 |
22
+ | any other failure | `isUltimateError(error)`, then switch on `error.code` | shipped |
23
+ | install realtime in an island | nothing: `x build` does it for every island whose own graph imports `@ultimat3/realtime`. An island that reaches realtime only through a package calls `installRealtime({ signal: createSignal })` in `mount` | shipped in 21.0.0 |
24
+ | show one record in a component | `useRecord('post', id)`: record type, then record key, from `@ultimat3/realtime` | shipped in 21.0.0 |
25
+ | show a list | `useQuery({ name: 'postList', entity: 'post' }, input)`, or `{ name, live: true }` for a live query | shipped in 21.0.0 |
26
+ | read once, no socket | `useQuery({ name: 'postList', entity: 'post' }, input)` with no `live` — one HTTP GET through the query client, rows into the same store. Nothing opens a socket (`packages/realtime/src/use-query.ts`): a list that needs no push needs no timer either — never poll | shipped in 21.0.0 |
27
+ | re-read a list on a timer | `useQuery(ref, input, { poll: pollEvery(ms) })` on a non-live ref — `refetch` through core's `visibleInterval`, in the bundle only of an island that imports `pollEvery`: no read while the tab is hidden, one make-up read when it shows, none after a lost session, stopped by `release()`. The safety read beside an at-most-once channel. A live ref refuses `poll` by type. Never a hand-rolled `setInterval` | shipped in 30.0.0 |
28
+ | poll what has no changefeed | `visibleInterval(run, ms, { maxMs? })` from `@ultimat3/core/page`, the disposer from `onCleanup`: no call while the tab is hidden, one make-up call when it shows; with `maxMs` each failed run (a throw or rejection, re-raised for the page's error hook) doubles the wait up to it, jittered, and a success resets it to `ms`; an async `run` is never overlapped. The first `X_UNAUTHENTICATED` / `X_SESSION_EXPIRED` on a page rendered for a principal is a lost session: every `visibleInterval` stops, `sessionIsLost()` is `true` and `onSessionLost(fn)` fires once (late subscribers at once) until `rescope()`. An anonymous page's 401 is never a loss. The sign-in UI is the app's. `guards/raw-interval.ts` refuses a bare `setInterval(` in an island | shipped in 30.0.0 |
29
+ | follow a channel | declare it on one ref in two halves: `channelRef('org-feed', { params, catchUp })` for the browser, `channel(ORG_FEED, { records, policy })` on the server. Then `useChannel(ORG_FEED, { orgId }, { onEvent, onPresence })` in the island: records reach the store, events the handlers. `usePresence(ORG_FEED, params)` for a roster. An island whose only realtime is this is never hidden while it boots ([Realtime](Realtime#which-islands-wait-for-the-pages-restore)) | shipped in 21.0.0 |
30
+ | redraw markup that holds other islands | `disposeIslands(node)` from `@ultimat3/render/client` on each element being dropped, before or after it leaves the page: every island under it (and the node itself, when it is one) has what its `mount` returned called, and one that never booted mounts nothing. Never read `el.__x` yourself | shipped in 27.6.0 |
31
+ | sign out | an action (the reference app's `endSession`, `POST /api/sessions/end`), posted by a native form, whose response appends `signOutHeaders()` from `@ultimat3/auth`: `Clear-Site-Data: "cache", "storage"` empties the page store, local storage and the service worker's cache. The redirect is a full navigation, so the next document carries the new scope | shipped in 21.0.0 |
32
+ | keep a record type on disk across reloads | `entity('post', { columns, persist: true })`. Default `false`: a record is private data, and disk is a decision | shipped in 21.0.0 |
33
+ | write with an optimistic update | `useMutation({ name: 'renamePost', local, conflict })`, posted over HTTP | shipped in 21.0.0 |
34
+
35
+ Never write `fetch(`, `new WebSocket(` or `new XMLHttpRequest(` in an island, or in anything an
36
+ island imports. `x verify`'s `boundaries` step refuses it with `X_BROWSER_TRANSPORT_BYPASS` and
37
+ names the line — in every app, `As of 2026-10`. Built in, not a file in `guards/`: there is no
38
+ opt-out and no allowlist.
39
+
40
+ | The raw call was | Write instead |
41
+ |---|---|
42
+ | an action (`POST /api/…`) | `await browserClient.<action>(input)` — `rpc<Api['actions']>` in `shared/browser-client.ts` |
43
+ | a read (`GET /_x/query/…`) | `useQuery(<QUERY_REF>, input)`; outside a component, `await browserQueries.<query>(input)` |
44
+ | a socket or an `EventSource` | `useQuery(<QUERY_REF>, input)` for rows, `useChannel(<CHANNEL_REF>, params, { onEvent })` for events |
45
+ | a file upload | `await uploadFile({ file, grant, onProgress })` from `@ultimat3/storage` — the presigned PUT is its, not yours |
46
+ | anything else | `await clientTransport({ method: 'GET', url })` from `@ultimat3/core/page` |
47
+ | a page or fragment that answers HTML, CSV or plain text | `await clientTransport({ method: 'GET', url, responseType: 'text' })` — the body as a `string`, never read off `onResponse` |
48
+
49
+ | Fact | Rule |
50
+ |---|---|
51
+ | browser-reachable | every `*.island.tsx`, every module that calls `clientTransport` / `pageClient` itself, and what each imports — followed name by name through barrels, into the app's own `packages/*` |
52
+ | not reported | a `route.ts`, a job, a task, a test: server code no island imports |
53
+ | the package boundary | the walk stops at `node_modules`. `@ultimat3/*`'s three seams are trusted, never re-read |
54
+ | a server barrel | `@ultimat3/entity` or `@ultimat3/query` imported for a VALUE in that closure is `X_BROWSER_SERVER_BARREL`; the fix names `@ultimat3/entity/record` / `@ultimat3/query/client` |
55
+ | cannot see | a browser module that is no island, names no seam and is imported by neither; a request inside an installed package; `globalThis['fetch']`; a bare `fetch(` in a file that binds its own `fetch` |
56
+
57
+ ## 1. The entity is the record
58
+
59
+ ```ts
60
+ // app/posts/entity.ts
61
+ import { entity, text, uuid } from '@ultimat3/entity';
62
+
63
+ export const Post = entity('post', {
64
+ columns: { id: uuid().primaryKey(), orgId: uuid(), title: text({ max: 120 }) },
65
+ });
66
+ ```
67
+
68
+ The record type is the entity name (`post`). The record key is the primary key, in declared order.
69
+ Nothing else is declared.
70
+
71
+ ## 2. An action that returns the row
72
+
73
+ ```ts
74
+ // app/posts/actions/rename-post.ts
75
+ import { action, t } from '@ultimat3/action';
76
+ import { Post } from '../entity';
77
+ import { postWrite } from '../policies';
78
+ import { repo } from '../repo';
79
+
80
+ export const renamePost = action({
81
+ input: t.object({ postId: t.uuid, title: t.string }),
82
+ output: Post.$schema, // or t.object({ post: Post.$schema.nullable() }): found at any depth
83
+ policy: postWrite,
84
+ handle: ({ input }) => repo.rename(input.postId, input.title), // returns the WHOLE row
85
+ });
86
+ ```
87
+
88
+ Because the output schema references `Post.$schema`, the response is `{ data, records }` with the
89
+ header `x-ultimate-records: 1`. Nothing to declare. `.pick()`, `.omit()` or `.extend()` on the row
90
+ schema drops the brand, so a partial row never overwrites a full record.
91
+
92
+ ## 3. A query whose rows are records
93
+
94
+ ```ts
95
+ // app/posts/queries.ts
96
+ import { from, query, t } from '@ultimat3/query';
97
+ import { Post } from './entity';
98
+ import { postRead } from './policies';
99
+ import { repo } from './repo';
100
+
101
+ type PostRow = typeof Post.$row;
102
+
103
+ export const postList = query({
104
+ input: t.object({ orgId: t.uuid }),
105
+ policy: postRead,
106
+ rows: Post.$schema, // without this line the rows never reach the store
107
+ sql: ({ orgId }) =>
108
+ from<PostRow>('posts', () => repo.list()).where({ orgId }).orderBy('id'),
109
+ });
110
+ ```
111
+
112
+ `sql:` names its table as a string, so `rows:` is the one place the schema comes from. Leave it
113
+ off and the query still works, but its answer is bare rows the store never sees.
114
+
115
+ ## 4. Call both from an island
116
+
117
+ ```ts
118
+ // shared/browser-client.ts — type-only import of the api, so no server code reaches the island
119
+ import { rpc } from '@ultimat3/action';
120
+ import { queryClient } from '@ultimat3/query/client'; // the browser entry: 11,012 B vs 25,237 B via the barrel
121
+ import type { Api } from '../api';
122
+
123
+ export const browserClient = rpc<Api['actions']>({ baseUrl: '' });
124
+ export const browserQueries = queryClient<Api['queries']>({ baseUrl: '' });
125
+ ```
126
+
127
+ | Fact | Rule |
128
+ |---|---|
129
+ | who writes the file | `x new` — `apps/web/shared/browser-client.ts`, both lines, with `browser-client.test.ts` beside it |
130
+ | app size | both lines typecheck at any size — pinned at 300 actions in 100 modules and 100 reads in 50 (`client-scale-pins.ts` in each package). `As of 2026-10`; before, a 48th module in one `defineApi` list was TS2589 |
131
+ | `pathStyle` | stated nowhere here. The server stamps `defineApi({ http: { pathStyle } })` into the document; the browser derives every action URL under it |
132
+
133
+ ```ts
134
+ // app/posts/rename.island.tsx
135
+ import { isSuperseded, isUltimateError } from '@ultimat3/core';
136
+ import { browserClient } from '../../shared/browser-client';
137
+
138
+ export async function rename(postId: string, title: string): Promise<void> {
139
+ try {
140
+ await browserClient.renamePost({ postId, title }); // returns `data`, the envelope is stripped
141
+ } catch (error) {
142
+ if (isSuperseded(error)) return; // the page changed principal: render nothing
143
+ if (isUltimateError(error) && error.code === 'X_CLIENT_TRANSPORT_FAILED') {
144
+ // no usable answer: the network, a proxy, or a non-JSON body
145
+ }
146
+ throw error;
147
+ }
148
+ }
149
+ ```
150
+
151
+ `baseUrl: ''` is the page's own origin. A browser chunk has no `process.env`.
152
+
153
+ ## How a record travels
154
+
155
+ | # | Where | What happens | Status |
156
+ |---|---|---|---|
157
+ | 1 | server | the action or query route runs `rowsOf(schema, answer)`, collecting every branded row as record type → record key → row | in tree |
158
+ | 2 | wire | `{ data, records, removed? }` behind `x-ultimate-records: 1`. An answer with no entity rows is byte-identical to before | in tree |
159
+ | 3 | browser | `clientTransport` decodes the envelope, checks the principal has not changed, and calls `pageClient().store.adopt(type, rows)` | in tree |
160
+ | 4 | browser | the one `RecordStore` per tab, shared by every island through `globalThis[Symbol.for('ultimate.client')]`, updates the record once | in tree (`packages/realtime/src/page-store.ts`). Installed by the first realtime hook on the page. Until one runs, step 3 finds no store and the page **holds** the records (`packages/core/src/pending-records.ts`), handing them over on install |
161
+ | 5 | component | every `useRecord` / `useQuery` showing that record re-renders, with no refetch | in tree |
162
+
163
+ ## Errors you will hit
164
+
165
+ | Code | When | Fix |
166
+ |---|---|---|
167
+ | `X_CLIENT_TRANSPORT_FAILED` | no usable answer: the network refused, a proxy answered a non-2xx with no framework code, or a 2xx body was not JSON | `x doctor --json` checks the network and the gateway in front of the app, then retry. A write with no `idempotencyKey` may already have landed |
168
+ | `X_CLIENT_SCOPE_CHANGED` | the page changed principal while a read was in flight | `if (isSuperseded(error)) return;`, then read again under the new principal |
169
+ | `X_CLIENT_RECORD_ENVELOPE_INVALID` | a response carried `x-ultimate-records: 1` and a body of the wrong shape | build the body with `encodeRecordEnvelope()` from `@ultimat3/core` in the handler that set the header, or stop setting the header |
170
+ | `X_RECORD_KEY_MISSING` | a returned row lacks a primary-key column | `x entities show <entity> --json` lists the key. Return the whole row |
171
+ | `X_MUTATOR_CLOCK_MISSING` | a mutator declares `conflict: 'last-write-wins'` and its entity has no number `updatedAt`, or its output has no entity row | add `updatedAt` (a number, epoch ms, written by the server) to the entity, or declare `conflict: 'server-wins'` |
172
+ | `X_REALTIME_UNINSTALLED` | a realtime hook ran in an island that never called `installRealtime()` | `x build`. If the island reaches realtime only through a package, call `installRealtime({ signal: createSignal })` in its `mount` |
173
+ | `X_RECORD_REJECTED` | a row reached the store with no key, or not as an object | `x entities show <entity> --json` lists the key. Return whole rows |
174
+ | `X_CONTRACT_DRIFT` | the client bundle and the server are on different builds | reload the page to pick up the new client bundle |
175
+
176
+ Full rows: [Error codes](Error-Codes).
177
+
178
+ ## Shared island chunks
179
+
180
+ **`islands: { sharedChunks: true }` in `app.config.ts` builds every island in one split build**, so
181
+ a module two islands import is one `/islands/chunk-<hash>.js` a page fetches once. Off by default,
182
+ by measurement (`packages/core/src/config-islands.ts`): tree shaking across one build keeps what
183
+ any importer uses, so an island that takes one helper from a module the others use heavily pays for
184
+ all of it — the reference app's plain-DOM update banner went 712 → 16,288 B with it on.
185
+
186
+ | Turn it on when | Leave it off when |
187
+ |---|---|
188
+ | one surface's pages render several islands over one shared graph (the same form kit, the same chart) | an island is small and plain-DOM, or the islands share little |
189
+ | `x build --target static`, then `x verify --only budgets`, shows the routes that render several islands getting smaller | any route's `budget.js` grows past its budget |
190
+
191
+ Layering: the last config layer that states `sharedChunks` wins; absent everywhere is `false`
192
+ (`mergeIslands`). A value that is not a boolean is refused at boot (`X_CONFIG_INVALID`). `As of
193
+ 2026-10`.
194
+