@ultimat3/cli 29.4.0 → 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 +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 +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,150 @@
1
+ # Project layout
2
+
3
+ `x new` generates a monorepo, so mobile/desktop/extension can be added later without restructuring.
4
+
5
+ ```
6
+ myapp/
7
+ apps/
8
+ web/ # the Ultimate app — the three surfaces live here
9
+ site/ # static/isr, 0kb JS baseline, SEO-critical
10
+ app/ # auth'd, stream/ssr, realtime, heavy
11
+ api/ # actions only, no rendering
12
+ shared/ # tokens, primitives, entity types, policies
13
+ admin/ # generated admin dashboard (Ultimate app, role=web)
14
+ mobile/ # placeholder + README (native Swift/Kotlin later)
15
+ desktop/ # placeholder + README (Tauri/Electron later)
16
+ packages/
17
+ domain/ # pure types + constants, no I/O
18
+ db/ # entity() declarations + plain-SQL migrations, no business logic
19
+ i18n/ # app catalogs (en, es, ...)
20
+ ui/ # app-specific Solid components on top of @ultimat3/ui
21
+ mcp/ # the app's own MCP tools (its dashboards are AI-first too)
22
+ bin/ # setup, dev, check — thin wrappers over `x`
23
+ guards/ # one rule per file, DISCOVERED not registered — `x verify` runs
24
+ # each inside its `boundaries` step. `x new` writes every
25
+ # name in SHIPPED_GUARD_NAMES (packages/cli/src/templates/
26
+ # scaffold-guards.ts); `x doctor` lists the ones missing
27
+ docker/ # Dockerfile, Dockerfile.dockerignore, both compose files, and
28
+ # helm/ — the chart, 8 files. All written by `x new`
29
+ app.config.ts # the one config file
30
+ x.manifest.json # GENERATED: routes, entities, actions, jobs, policies
31
+ CLAUDE.md # short, human-authored conventions
32
+ AGENTS.md # a symlink to CLAUDE.md: one source for every agent
33
+ ```
34
+
35
+ ## Surfaces
36
+
37
+ Four directories, two bundle graphs, one hard boundary.
38
+
39
+ | Surface | Audience | Default render | JS baseline | Auth | May import |
40
+ |---|---|---|---|---|---|
41
+ | `site/` | anonymous, crawlers | `static` / `isr` | **0kb** | none | `shared/` |
42
+ | `app/` | signed-in users | `stream` (or `ssr`) | whatever the budget allows | required | `shared/`, `api/` types |
43
+ | `api/` | programs, agents, the typed client | none | n/a | policy per action | `shared/` |
44
+ | `shared/` | both | n/a | must stay 0-dep-heavy | n/a | nothing app-local |
45
+
46
+ ## `site/` cannot import `app/`
47
+
48
+ A build error. `X_BOUNDARY_VIOLATION`, with the offending import chain printed:
49
+
50
+ ```
51
+ X_BOUNDARY_VIOLATION: site/ imported app/
52
+ cause: site/pricing/page.tsx → shared/ui/button.tsx → app/charts/sparkline.tsx → chart.js
53
+ fix: x fix boundary site/pricing/page.tsx (or move sparkline out of shared/ui)
54
+ ```
55
+
56
+ The failure it prevents: `<Button>` lives in `shared/ui/`; someone adds `<Sparkline>` to the same file, importing a charting library; `site/pricing` imports `<Button>`; the highest-intent page in the product now ships the chart library. Nothing broke, nothing warned, LCP regressed 900ms, found a quarter later. **The import that costs you is three hops from the file anyone reviewed** — which is why the check resolves transitively.
57
+
58
+ Enforcement, not vigilance:
59
+
60
+ | Rule | Check |
61
+ |---|---|
62
+ | `site/` → `app/` import | build error, transitive chain resolved (not just direct imports) |
63
+ | `shared/` → `app/` or `site/` import | build error — `shared/` is a leaf |
64
+ | A `shared/` module exceeding its own byte budget | build error, so `shared/ui` cannot silently fatten |
65
+ | `site/` route emitting >0kb JS without an explicit `hydrate` | build error |
66
+ | `app/` → `api/` | **types only** (`import type`); a runtime import is a build error, call the typed client instead |
67
+ | routes → DB | build error. Routes call actions/queries; only `repo.ts` touches the DB |
68
+ | components → business logic | build error via the feature-slice rule below |
69
+ | services → HTTP | build error. A service that knows about requests cannot be reused by a job |
70
+
71
+ `scripts/boundaries.ts` runs in `x verify` **and** in the dev server, so the failure arrives while you are typing. `/_x` → **Boundaries** renders the same graph with violations highlighted.
72
+
73
+ ## 0kb JS on `site/`
74
+
75
+ Default for every `site/` route: `render: 'static' | 'isr'`, `hydrate: 'never'`. Inlined critical CSS, no script tag.
76
+
77
+ | Need | Solution — no framework JS |
78
+ |---|---|
79
+ | Mobile nav toggle | `<details>` / CSS `:has()` / checkbox hack |
80
+ | Newsletter form | native `<form method="post">` to an `api/` action |
81
+ | Carousel | CSS scroll-snap |
82
+ | Theme toggle | one inlined `<script>` under a documented byte cap, tokens flip via `data-theme` |
83
+ | Anything genuinely interactive | `hydrate: 'visible'` on **that one island**, with a per-route `budget.js` |
84
+
85
+ Opting a `site/` route into hydration is allowed, explicit, and budgeted — never silent. The starter landing page lives in `site/` on purpose: the framework eats its own static path, so a regression there breaks the template on day one instead of rotting quietly.
86
+
87
+ ## Feature slicing inside a surface
88
+
89
+ Feature-sliced, not layer-sliced. One folder per feature, one job per file.
90
+
91
+ ```
92
+ apps/web/app/<feature>/{entity,repo,service,policy,errors}.ts
93
+ apps/web/app/<feature>/{actions,live,queries,jobs,tasks}/<name>.ts # one per file, as generated
94
+ apps/web/app/<feature>/ui/ # components
95
+ ```
96
+
97
+ **One primitive per file, in its kind's directory** — `actions/<verb>-<name>.ts`, what `x g` writes, and what both tracked apps keep (`As of 2026-10`, owner decision 10 of #648). A primitive is registered by `defineApi()` and found by the module scan, never by its filename; what IS refused is a primitive module beside its directory — `actions.ts` with `actions/`, or `live`, `queries`, `jobs`, `tasks` — (`X_LAYOUT_SIBLING_MODULE`, the `boundaries` step), because `./actions` resolves to the file and never the directory. The filenames that ARE enforced:
98
+
99
+ | File | Owns | Never — and the error |
100
+ |---|---|---|
101
+ | `entity.ts` | table + domain type + invariants | I/O, policy |
102
+ | `repo.ts` | the feature's reads and writes, through the typed handle | a raw SQL literal the handle can express — `guards/repo-raw-sql.ts` |
103
+ | `service.ts` | business logic, composed from repos | an HTTP import — `X_BOUNDARY_SERVICE_TO_HTTP` |
104
+ | `page.tsx` · `layout.tsx` | a URL | a database import — `X_BOUNDARY_ROUTE_TO_DB` |
105
+ | `actions/` | `action` / `mutator` declarations, one per file | logic (delegate to `service.ts`) |
106
+ | `live/` · `queries/` | `query` declarations, one per file | writes |
107
+ | `jobs/` · `tasks/` | `job` / `task` declarations, one per file | inline slow work in an action |
108
+ | `policy.ts` | `policy` rules for this feature | data shaping |
109
+ | `ui/` | Solid components | fetching, business logic, its own authz |
110
+
111
+ A feature imports another feature through that feature's `service.ts` or its published types, never its `repo.ts` — a convention, **not a build error** (`As of 2026-10-06`: no boundary rule reads it from a `service.ts`). A **route** importing any `repo.ts` is a build error, `X_BOUNDARY_ROUTE_TO_DB`; `x new`'s own dashboard reads through the `postList` query (`../post/live/post-list`).
112
+
113
+ ## App packages
114
+
115
+ | Package | Rule |
116
+ |---|---|
117
+ | `packages/domain` | pure types + constants, **no I/O** |
118
+ | `packages/db` | `entity()` declarations + plain-SQL migrations, **no business logic**; no ORM in the request path ([Entities and migrations](Entities-And-Migrations)) |
119
+ | `packages/i18n` | flat catalogs; a missing key renders `⟦key⟧` and fails `x verify` ([I18n](I18n)) |
120
+ | `packages/ui` | app components on `@ultimat3/ui`; same byte budgets as `shared/` ([Theming](Theming)) |
121
+ | `packages/mcp` | the app's own MCP tools ([MCP and AI](MCP-And-AI)) |
122
+
123
+ ## Framework package tiers
124
+
125
+ Inside the framework repo, a package may import from **strictly lower** tiers only — never sideways within its tier unless listed, never upward. `scripts/boundaries.ts` makes a violation a build error.
126
+
127
+ | Tier | Packages | May import |
128
+ |---|---|---|
129
+ | 0 | `core`, `schema` | nothing internal |
130
+ | 1 | `i18n`, `money`, `time`, `cache`, `seo`, `db`, `storage`, `flags` | tier 0 |
131
+ | 2 | `entity`, `policy`, `http`, `auth` | tier 0–1 |
132
+ | 3 | `action`, `query`, `jobs`, `realtime` | tier 0–2 |
133
+ | 4 | `render`, `pwa`, `mcp`, `ai`, `manifest`, `mail`, `ui`, `notify`, `scraping` | tier 0–3 |
134
+ | 5 | `admin`, `testing`, `cli` | tier 0–4 |
135
+
136
+ The declared sideways edges are `SIDEWAYS_ALLOW` in [`scripts/lib/tiers.ts`](https://github.com/developerz-ai/ultimate/blob/main/scripts/lib/tiers.ts), the executable copy of both tables; [`llms.txt`](https://github.com/developerz-ai/ultimate/blob/main/llms.txt) carries a generated copy of the list. `admin → ui` was deleted 2026-08-19 by moving `ui` from tier 5 to 4 — the edge existed only to undo a placement two tiers above what `ui` actually imports.
137
+
138
+ Per-package layout is fixed: `package.json`, `tsconfig.json`, `README.md`, `CLAUDE.md`, `src/index.ts` (explicit exports, no `export *` outside pure-type modules), `src/errors.ts` (this package's `X_*` codes), one `src/<concern>.ts` per responsibility with `<concern>.test.ts` beside it. Target < 200 LOC per file, hard ceiling ~500. See [Contributing](Contributing).
139
+
140
+ ## Generated vs authored
141
+
142
+ | Path | Author | Drift behavior |
143
+ |---|---|---|
144
+ | `x.manifest.json` | generated every build | `x verify` fails on staleness |
145
+ | `openapi.json` | generated | contract diff fails on a breaking change without a version bump (`X_CONTRACT_DRIFT`) |
146
+ | `packages/db/migrations/` | `x db gen "<name>"` | schema mismatch is `X_DB_DRIFT` |
147
+ | `apps/web/**/sw.js` | derived from the route table | regenerated on `route` config change ([PWA and offline](PWA-And-Offline)) |
148
+ | `app.config.ts`, `AGENTS.md`, `CLAUDE.md` | you | never auto-appended; LLM-written context files reduce task success |
149
+
150
+ Repo files: [`docs/idea/06-surfaces.md`](https://github.com/developerz-ai/ultimate/blob/main/docs/idea/06-surfaces.md), [`docs/architecture/00-conventions.md`](https://github.com/developerz-ai/ultimate/blob/main/docs/architecture/00-conventions.md).
@@ -0,0 +1,429 @@
1
+ # Queries and live queries
2
+
3
+ A `query` is a read. `live: true` makes it subscribable. Never writes, never enqueues, never sends mail.
4
+
5
+ `As of 2026-08-23`. Stable API — semver from here ([Upgrading](Upgrading)). All three tiers of [Realtime](Realtime) ship. Tier 3 (local-first) shipped in 21.0.0, opt-in: `persist` is an `entity()` option, read by realtime's IndexedDB store and one outbox ([Realtime](Realtime#tier-3-shipped-in-2100)). `persist` is **not a field `query()` accepts**; writing it is a `TS2353` excess property.
6
+
7
+ ## The canonical shape
8
+
9
+ ```ts
10
+ // query
11
+ export const liveFeed = query({
12
+ input: t.object({ orgId: t.uuid }),
13
+ policy: can('feed:read'),
14
+ live: true,
15
+ sql: ({ orgId }) => db.posts.where({ orgId }).orderBy('createdAt').limit(50),
16
+ });
17
+ ```
18
+
19
+ ## Fields
20
+
21
+ | Field | Required | Rule |
22
+ |---|---|---|
23
+ | `input` | yes | Standard Schema; `t` re-exported from `@ultimat3/query`, so a query file imports one package. The shipped provider is `@ultimat3/schema`'s dependency-free builtin — ArkType, Zod and Valibot are optional swaps behind `configureSchemaProvider`, and no adapter ships. Parsed before `policy`, before `sql`. Becomes the GET query string, the client hook argument, and the MCP tool's JSON Schema |
24
+ | `policy` | yes | `can('<perm>')`, optionally with a predicate over `{ input, actor }`. Evaluated at HTTP call, client hook, subscribe, **and per delivered row** |
25
+ | `live` | no — default `false` | registers the query with the incremental matcher. Requires a deterministic, bounded `sql` |
26
+ | `single` | no — default `false` | `true` declares a read of ONE object (a detail page's row by id or slug). Only the wire changes: the HTTP GET answers the first row `sql` returns as the body, **404 `X_NOT_FOUND`** when there is none (a list read answers `200 []`), refuses `_first`/`_after` (400), and `openapi.json` documents one object, a `404` and no page controls. The typed client answers `Promise<TRow>` and has no `.page`. The MCP tool answers as the route does — the row, or `X_NOT_FOUND` — over `tools/call` alike (`As of 2026-10`). Every in-process caller — `read(input)`, `.as()`, `.page()`, `.live()` — keeps `readonly TRow[]`. Anything but a boolean is `X_QUERY_SINGLE_INVALID`. `As of 2026-09-29` |
27
+ | `sql` | yes, on a rows read — never beside `download` | `(input) => SqlSource`. `from()` (`@ultimat3/query`) wraps an already-resolved `@ultimat3/entity` repo call and restates `where`/`orderBy`/`limit` for the matcher to read back; `select`/`preload` happen inside that repo call, before `from()` ever sees a row. No ORM in the graph. SQL-transparent: `toSQL()` prints the statement verbatim so an agent can read it and self-correct |
28
+ | `rows` | no | the row's schema: `Post.$schema` or a `t.object({ … })`. The published contract of the answer: `openapi.json`, the mount's document, `x.manifest.json` and the MCP tool's `outputSchema` print its members (structure only, a sealed column absent). `x verify` refuses a member removed, retyped or made optional without a major. An **entity's** `$schema` also makes the answer the record envelope. Not a parse: a query's rows are never re-validated on the way out. Absent, or a schema that cannot be introspected, the document says only `items: {}`. `As of 2026-11` |
29
+ | `mcp` | no — default not exposed | `{ expose: true, description }` makes the read an MCP tool. Opt-in, unlike an action: a read hands rows to an agent, so silence exposes nothing |
30
+ | `mcp.visibleTo` | no | roles that may see the projected tool; a caller whose role is not named gets ToolNotFound, never Forbidden — the policy still decides every call |
31
+ | `errors` | no — `x g query` writes `errors: []` | the 4xx codes a reader may branch on, closed: an action's rule ([Actions](Actions#declared-refusals-errors)), judged around `sql` and the source's `execute`. Undeclared in a test or dev run is `X_ERROR_UNDECLARED`; `.contract()` scans `sql`'s own source; `openapi.json` and the MCP tool name each code. `As of 2026-11` |
32
+ | `audit` | no — default `false` | `true` records every call in the installed `AuditSink` — allowed, denied, failed, and a memo or cache hit (`replayed: true`). See [Audit](#audit-who-saw-what) |
33
+ | `http.path` | no | pins the read's URL in place of `/_x/query/<kebab>`: `http: { path: '/orders/:id' }`. Lowercase static segments and `:param`s, never `/_x`; each `:param` a REQUIRED `t.string` key of `input:` (`X_QUERY_HTTP_PATH_INVALID` at `query()`, as is an `http.method` other than `'GET'`). The URL's value joins the search string before the input is parsed; a search string naming the key differently is **400** `X_HTTP_PATH_INPUT_CONFLICT`. `client()` fills the URL; `queryClient` by name cannot see a pin. `openapi.json` publishes `{id}` `in: path`, the key gone from the query parameters. `As of 2026-11` |
34
+ | `etag` | no | `true` answers `GET` with a strong `ETag` (a SHA-256 of the body and the actor's authority, so one actor's validator never matches another's) and `cache-control: private, no-cache`; a matching `If-None-Match` is a bodiless **304**, decided AFTER the policy and the rate limit — a 304 is still a read of that person's rows, so a denied caller is 403 and the 304 spends the bucket. Opt-in: it moves the answer from `no-store` to the browser's disk. `openapi.json` documents the header, the `ETag` and the 304. On a `live` read, or beside `download`, `X_QUERY_ETAG_INVALID`. `As of 2026-11` |
35
+ | `timeoutMs` | no | this read's own `statement_timeout`, tighter than the role's: the source runs inside `withStatementTimeout` (`@ultimat3/db`), one read-only transaction with a transaction-local bound. **A `timeoutMs` read skips replicas** — a transaction is pinned to the primary. Firing is `X_DB_STATEMENT_TIMEOUT`; above the bound in force is `X_DB_STATEMENT_BOUND_INVALID` (a read cannot extend what the role was sized for); not a whole positive number, `X_QUERY_TIMEOUT_INVALID` at `query()`. `As of 2026-11` |
36
+ | `download` | no | `{ handle, expiresInMs? }` in place of `sql`: the read answers a FILE at a short-lived signed URL. Every rows field beside it is `X_QUERY_DOWNLOAD_INVALID`. See [A file at a signed URL](#a-file-at-a-signed-url--download) |
37
+ | cache tags | derived | acquired automatically from the tables `sql` touches. Never hand-declared on a query |
38
+
39
+ Nothing else is a query field. Sorting, paging, and filtering are `input` fields consumed by `sql`.
40
+
41
+ ## The fluent surface
42
+
43
+ Every projection this package owns is a method on the query — `liveFeed.live(input)`, never `toLiveQuery(liveFeed, input)` — and every declared field is lifted onto it. The MCP read tool is `@ultimat3/mcp`'s one projection, `toolFrom(liveFeed)` (`As of 25.0.0` there is no `.tool()`). A query has no `.def`.
44
+
45
+ | Member | Is | Rule |
46
+ |---|---|---|
47
+ | `liveFeed(input, options?)` | the read | the policy's actor half → parse input → evaluate policy → build source → execute, through the cache tiers. A reader refused whatever they send is 401/403, never `X_INPUT_INVALID` |
48
+ | `.as(actor, input, options?)` | the same read, as someone else | keeps the surrounding context whole — services, clock, locale, trace — and swaps only the actor. `null` is the signed-out caller |
49
+ | `.page(input, { first, after? })` | one bounded page | `{ rows, nextCursor, hasMore }` — the one page shape, `@ultimat3/core`'s `Page`, the same `findMany` answers; the `endCursor`/`hasNextPage` aliases left in 25.0.0. `nextCursor` is `null` exactly when `hasMore` is false (25.0.0: the last page used to keep a cursor), so `while (page.hasMore)` stops on the last page. The cursor is signed and scoped to `queryHash(name, input)` — the query's name and its parsed input, never `first` or `after`, which are controls rather than scope. There is no `offset` and there never will be. A `repo.ts` raw join pages with `keyset()` from `@ultimat3/db` ([Entities and Migrations](Entities-And-Migrations#paging-a-raw-join)): the same `Page`, its own surface |
50
+ | `.live(input, options?)` | the subscription descriptor | a `LiveQuery` carrying the **same** policy object, re-evaluated per subscriber |
51
+ | `.client({ baseUrl })` | the typed browser method | `GET /_x/query/live-feed?orgId=…`, keys sorted so one input is one URL. `.client(…).page(input, { first, after })` reads one page over the same route |
52
+ | `.describe()` | the manifest row | name, capability, tags, ttl, `live` |
53
+ | `.input` `.policy` `.cache` `.mcp` `.isLive` | the declaration, lifted | readable. `sql` is not among them |
54
+
55
+ `sql` is unreachable by design. The declaration lives in a private store inside `read.ts` and `@ultimat3/query` exports no reader for it, so `sourceFor` is the only thing that can build a source — one read path and one authz path, structurally rather than by convention. A hand-rolled object with `kind: 'query'` is `X_QUERY_FOREIGN`, never a registered read.
56
+
57
+ `isLive` is the declared boolean; `live()` is the subscription itself. Every projection needs the name `registerQueries()` stamps on — before that, `X_QUERY_UNREGISTERED`.
58
+
59
+ ## Five projections
60
+
61
+ | Projection | Derived from | Shape |
62
+ |---|---|---|
63
+ | HTTP GET | name + `input` | `GET /_x/query/live-feed?orgId=…`, errors as `UltimateError` JSON |
64
+ | Read hook | the query's name, and `live` | `const feed = useQuery({ name: 'liveFeed', live: true }, { orgId })` in an island. It returns an `AsyncState` accessor; see below |
65
+ | Live subscription | `live: true` | WS frames `{qid, op, row, lsn}` patched into the page's one record store |
66
+ | Cache entry | tags from `sql` | key is the query name + a fingerprint of the parsed input + the sorted tag keys — the actor is not in it; see [Caching and invalidation](Caching-And-Invalidation) |
67
+ | MCP read tool | `input` + `policy` + name | one read tool per query, named for the export **verbatim** — `liveFeed`, never `live_feed`. The URL is kebab-cased; the tool name is not. Identical authz. See [MCP and AI](MCP-And-AI) |
68
+
69
+ ### The read hook, precisely
70
+
71
+ **One read hook, `useQuery`**, in 21.0.0. `useLive` and `liveHookFor` are deleted.
72
+ Live-ness belongs to the query declaration, not to the hook an island picks.
73
+
74
+ ```ts
75
+ import { useQuery } from '@ultimat3/realtime';
76
+
77
+ type FeedRow = { id: string; title: string };
78
+ declare const orgId: string;
79
+
80
+ const feed = useQuery<FeedRow>({ name: 'liveFeed', live: true }, { orgId });
81
+ // feed(): AsyncState<readonly FeedRow[]>; feed.refetch(); feed.release()
82
+ ```
83
+
84
+ | Fact | Rule |
85
+ |---|---|
86
+ | The ref | `{ name, live?, entity? }`: the registered name, never the query **value**. Importing the value drags its read path into the island; measured at 698,801 B for the reference feed |
87
+ | `live: true` | rows arrive and move over the page's one socket |
88
+ | no `live` | one `GET /_x/query/<kebab>` through `clientTransport`. Name its `entity` to make the rows store records that any write moves; without it the list holds its rows itself and only `refetch()` moves them |
89
+ | What it returns | an `AsyncState` accessor: `pending`, `refreshing` (keeps the rows on screen), `ready`, `failed` |
90
+ | Lists hold ids | the rows come from the page's `RecordStore`, so a record updated through any path re-renders every list holding it |
91
+ | Input | read **once**, at call time. A changed input is a new `useQuery` |
92
+ | `poll` | `pollEvery(ms, { maxMs? })` from `@ultimat3/realtime`; non-live only, refused by type on a live ref: `refetch` every `ms` through `visibleInterval` from `@ultimat3/core/page`, which only an island importing `pollEvery` carries — hidden tab skipped with one make-up read on show, stopped by a lost session and by `release()`. A non-finite value is `finiteOption`'s refusal |
93
+ | Lifetime | the caller owns `release()` (Solid: `onCleanup`) |
94
+ | Not derived, `As of 2026-09-22` | `live` and `entity` are stated on the ref by hand, and nothing checks them against the query declaration. A ref saying `live: true` for a non-live query subscribes to nothing |
95
+ | Types | the row type is the caller's annotation (`useQuery<FeedRow>`), not inferred from the query. That is the price of never importing the query value |
96
+ | A server render | answers `pending` with no subscription |
97
+ | A browser bundle that never called `installRealtime()` | `X_REALTIME_UNINSTALLED` |
98
+
99
+ ### The HTTP GET, precisely
100
+
101
+ Mounted for every registered query by `x dev` and by a container, from one composition — nothing to wire in the app.
102
+
103
+ | Fact | Rule |
104
+ |---|---|
105
+ | Method + path | `GET /_x/query/<kebab-export-name>`, the URL `.client()` derives with no server import — or the read's `http.path` pin, in its place |
106
+ | Input | the search string, coerced at the boundary (`t.number` from `"12"`) then validated by the query's own schema. Repeated keys are an array, and keys are sorted so one input is one URL. The typed client sends a `Date` as its ISO instant and nothing for an empty array; the route reads a **required** array nobody sent as `[]`, so `{ tags: [] }` arrives (`As of 2026-10`). An optional or defaulted array keeps the schema's own answer for absence — `[]` passed to one arrives as absent |
107
+ | Bad input | **400** `X_INPUT_INVALID`, with `x queries show <name> --json` as the fix — the same code and line every other surface of that read answers |
108
+ | Authz | evaluated once, inside the read, from the parsed input. `auth: 'public'` only for `allow()` — anything else is `required`, and an anonymous caller is 401 before the policy is reached |
109
+ | Caching | `no-store`. The URL names no actor while the rows are scoped to one, so a shared cache is something a CDN in front of the app configures knowingly. The read's own `cache:` tags ride along for a purge. `etag: true` is the declared exception: `private, no-cache` and a 304 for a held validator. The browser's own cache revalidates, so `useQuery` needs no code |
110
+ | Failures | `application/problem+json` carrying the code, cause and fix. A non-framework throw is the server's 500, never dressed as a read failure |
111
+ | A page | `?_first=20` answers `.page()`'s own envelope — `{ rows, nextCursor, hasMore }` — and `&_after=<nextCursor>` continues it. Without a control the answer is the bare array. The keys carry an underscore because `first` is a legal input member (a listing's own page size); declaring `_first` or `_after` as input is `X_QUERY_INPUT_UNENCODABLE`. A size outside 1–10,000 or an `_after` without `_first` is **400** `X_INPUT_INVALID`; a cursor that is not this read's is **400** `X_CURSOR_INVALID`. `As of 2026-09`. **A declared `.limit()` is the size of the listing** (`As of 2026-10`): `_first` pages inside it and never past it, the last page inside it answers `hasMore: false` and `nextCursor: null`, and a read meant to be paged to the end declares no limit |
112
+ | In `openapi.json` | every read, as a `GET` path item: the input as `in: query` parameters, then `_first` and `_after`, and a `200` that is `oneOf` the rows and the envelope, each row carrying the `rows:` members inline. A `single: true` read: the input parameters only, a `200` that is the row (one object when no `rows:` is declared), a `404`, and `x-ultimate.single: true`. A bearer mount's document carries the same operation at its prefix |
113
+ | One object | `single: true` answers the row itself, or **404** `X_NOT_FOUND` — see below |
114
+
115
+ ### A read with a validator — `etag: true`
116
+
117
+ ```ts
118
+ // `postRecord` in examples/dummy: the like island re-reads it on every load of /posts/{id}.
119
+ import { can } from '@ultimat3/policy';
120
+ import { from, query, t } from '@ultimat3/query';
121
+
122
+ interface PostRow {
123
+ readonly id: string;
124
+ readonly orgId: string;
125
+ readonly likeCount: number;
126
+ }
127
+ declare const posts: readonly PostRow[];
128
+
129
+ export const postRecord = query({
130
+ input: t.object({ orgId: t.uuid, postId: t.uuid }),
131
+ policy: can('post:read'),
132
+ etag: true,
133
+ sql: ({ orgId, postId }) =>
134
+ from<PostRow>('posts', posts).where({ orgId, id: postId }).orderBy('id').limit(1),
135
+ });
136
+ // GET /_x/query/post-record?… → 200, ETag: "…", cache-control: private, no-cache
137
+ // GET /_x/query/post-record?… If-None-Match: "…" → 304, no body (same actor, same bytes)
138
+ ```
139
+
140
+ | Fact | Rule |
141
+ |---|---|
142
+ | Who sends `If-None-Match` | the browser, from its own cache, on every reuse — `useQuery` and `queryClient` carry no code for it. An API client sends the `ETag` it holds |
143
+ | Proved | `examples/dummy/apps/web/e2e/read-revalidates.e2e.test.ts`: across a real reload, the read reaches the server and crosses the wire as headers only (`transferSize` 300), the island still handed the whole body |
144
+ | When to declare it | a read re-asked while its answer rarely moves — a seed read, a poll. Not a read whose rows must never touch the device's disk |
145
+ | Not with | `live: true` (its cursor is its validator) or `download:` — `X_QUERY_ETAG_INVALID` at `query()` |
146
+
147
+ ### A read of one object — `single: true`
148
+
149
+ ```ts
150
+ export const postById = query({
151
+ input: t.object({ id: t.uuid }),
152
+ policy: can('post:read'),
153
+ single: true,
154
+ sql: ({ id }) => db.posts.where({ id }).limit(1),
155
+ });
156
+ // GET /_x/query/post-by-id?id=… → 200 { id, title, … } · 404 X_NOT_FOUND
157
+ // await postById.client({ baseUrl })({ id }) → Promise<Row>
158
+ // await postById({ id }) → readonly Row[] (unchanged in process)
159
+ ```
160
+
161
+ | Fact | Rule |
162
+ |---|---|
163
+ | The body | the FIRST row `sql` returns — the one every in-process `[0]` of the same read takes. Say `.limit(1)`: nothing counts the rest |
164
+ | No row | **404** `X_NOT_FOUND`, `@ultimat3/entity`'s code, whose fix prints the SQL. The policy runs first, so a caller it denies is still 403 |
165
+ | Page controls | `_first` or `_after` is **400** `X_INPUT_INVALID`, and neither is published |
166
+ | `rows:` | the envelope as usual, with the row as `data` |
167
+ | The MCP tool | the row itself, or `X_NOT_FOUND` — the route's answer on both counts, over `tools/call`. A declared `rows:` publishes the row as `outputSchema`, with no `rows` wrapper |
168
+ | Not changed | `read(input)`, `.as()`, `.page()` and `.live()` still answer rows. `useQuery` reads lists: a single read in an island is `.client()` or `queryClient` |
169
+
170
+ ### Two spellings of that client, one URL
171
+
172
+ `.client({ baseUrl })` binds one read; `queryClient<Api['queries']>({ baseUrl })` binds every registered read at once, the way `rpc<Api['actions']>` does for writes. Both run `queryClientMethodFor`, so the URL cannot differ between them.
173
+
174
+ ```ts
175
+ // apps/web/shared/client.ts — the app's one place for both
176
+ export const client = rpc<Api['actions']>({ baseUrl }); // writes
177
+ export const queries = queryClient<Api['queries']>({ baseUrl }); // reads
178
+ ```
179
+
180
+ | Fact | Rule |
181
+ |---|---|
182
+ | When the map-wide one is the only option | a surface that may not import the feature — `site/`, where an edge into `app/` is `X_BOUNDARY_VIOLATION`. `.client()` needs the query object; `queryClient` needs only the `Api` **type** |
183
+ | Why two clients and not one | actions and queries are two registries (`defineApi`'s `actions:` and `queries:` keys) answering two methods. A read taken off the action client is a name that does not exist on `Api['actions']` — a compile error, which is the point |
184
+ | Types | the read's own `input` and row type. A list read answers `readonly TRow[]`, `limit(1)` included; a `single: true` read answers `TRow` and rejects with `X_NOT_FOUND` when the route 404s |
185
+ | Failures | the server's own code, off `problem+json` — `X_INPUT_INVALID` stays `X_INPUT_INVALID`. A gateway answering HTML, a network fault and a 2xx body that is not JSON are `X_CLIENT_TRANSPORT_FAILED`, the same code an action's client answers. It was `X_RPC_FAILED` until 21.0.0 |
186
+ | Rows are JSON, with their instants revived | what the body parses to, plus every value the server answered as a `Date` handed back as one (`As of 2026-10`, #710): the route names them in `x-ultimate-dates` by path — one entry per column of a list — and `clientTransport` revives exactly those. The body and the OpenAPI document are unchanged; a client that ignores the header reads ISO strings. A query's rows are never parsed on the way out (a declared `rows:` publishes structure, it parses nothing), so the header is how the row type's `Date` stays true |
187
+ | `then` is not a read | the proxy answers `undefined` for it, so `await queries` resolves to the client instead of fetching `/_x/query/then`. Same rule in `rpc` |
188
+
189
+ ## A file at a signed URL — `download:`
190
+
191
+ `As of 2026-10-31`. A read whose answer is bytes — a recording, an export, an attachment, a sealed
192
+ object decrypted on the way out — is a `query` with `download:` instead of `sql:`. Same input,
193
+ same policy, same name, same route; never a base64 string inside a JSON answer.
194
+
195
+ ```ts
196
+ import { open } from '@ultimat3/core';
197
+ import { can } from '@ultimat3/policy';
198
+ import { query, queryClient, t } from '@ultimat3/query';
199
+ import { disk } from '@ultimat3/storage';
200
+
201
+ export const runRecording = query({
202
+ input: t.object({ runId: t.uuid }),
203
+ policy: can('recording:watch'),
204
+ download: {
205
+ expiresInMs: 600_000, // 1 000 … 3 600 000; default 300 000
206
+ async handle({ input }) {
207
+ const key = `runs/${input.runId}/recording`;
208
+ const { bytes } = await disk('debug').get(key);
209
+ const plain = await open(new TextDecoder().decode(bytes), { purpose: `run-debug:${key}` });
210
+ return { body: plain, contentType: 'video/mp4', filename: `run-${input.runId}.mp4` };
211
+ },
212
+ },
213
+ });
214
+
215
+ // an island: mint, then hand the URL to the element that plays or saves it
216
+ declare const video: { src: string };
217
+ const client = queryClient<{ runRecording: typeof runRecording }>({ baseUrl: '' });
218
+ const { url } = await client.runRecording.url({ runId: '00000000-0000-4000-8000-000000000001' });
219
+ video.src = url;
220
+ ```
221
+
222
+ | Step | What happens |
223
+ |---|---|
224
+ | `client.runRecording.url(input)` | `GET /_x/query/run-recording?runId=…` with no `_token` — the MINT. The policy decides as for any read (401/403, the declared `rateLimit:`, `audit: true` records it); the answer is `{ url, expiresAt }` |
225
+ | the token | the claims — query, `fingerprint()` of the parsed input, expiry on `ctx.now()`, the minting actor without `facts` — `seal()`ed with the app master key (AES-256-GCM, purpose bound to the query). The ACTOR is ciphertext: no id, role or grant is readable. The query's path and its input are plain in the URL, as on every read — keep a secret out of a download's input |
226
+ | `GET <url>` | the SERVE, to whoever holds the URL — a `<video>`, a tab, a caller's own user — with no session. Seal checked first (`X_QUERY_DOWNLOAD_TOKEN_INVALID`, 403), then expiry (`X_QUERY_DOWNLOAD_TOKEN_EXPIRED`, 410), then the URL's input against the bound fingerprint (403). `handle` runs as the minting actor |
227
+ | the answer | the bytes, `content-disposition` (`attachment` default, `inline` on request; ASCII `filename` + exact `filename*`), `nosniff`, `content-security-policy: default-src 'none'; img-src 'self'; media-src 'self'; sandbox` (an inline file loads nothing elsewhere), `referrer-policy: no-referrer`, `cache-control: no-store`; `content-length` when the size is known; one `Range:` window as 206, 416 past the end |
228
+
229
+ `handle` answers `{ body: Uint8Array | Blob | ReadableStream, contentType, filename, disposition?, size? }`, or `body: (range?) => BodyInit` with `size` required. A `Uint8Array` or a `Blob` (a `Bun.file`, sliced lazily) is always range-served; a stream only with `size`, else whole with `accept-ranges: none`, and a window of one reads and drops its prefix. The function form opens only the window: `(range) => range === undefined ? disk.stream(key) : disk.streamRange(key, [range.start, range.end])`. A sealed object cannot take it, since an envelope opens whole. A sealed object is one AES-GCM envelope, so it is read with `get()` (bounded by `maxGetBytes`) and opened whole before a byte is served.
230
+
231
+ `.live()`, `.page()`, `.as()`, `sourceFor` and an MCP tool are `X_QUERY_DOWNLOAD_ONLY`; the
232
+ manifest marks the read `download: true` (rows ↔ download is breaking), and `x routes` lists it
233
+ on the read's own `GET /_x/query/<kebab>`. In-process, `await runRecording(input, { ctx })` is
234
+ the `DownloadFile` and `await runRecording.url(input, { ctx })` the path.
235
+
236
+ ## Owns / never
237
+
238
+ | Aspect | Rule |
239
+ |---|---|
240
+ | Projects to | HTTP GET, typed client hook, live subscription, cache entry with tags, MCP read tool |
241
+ | Owns | result shape + row-level filtering |
242
+ | Never | write, enqueue a job, send mail, read headers or cookies, authorize inside `sql` |
243
+ | Never | return partial data to satisfy a policy — filter rows in `sql`, decide yes/no in `policy` |
244
+
245
+ ## `live: true` requires deterministic, bounded SQL
246
+
247
+ `x verify` rejects a live query without both an `orderBy` and a `limit`.
248
+
249
+ | Requirement | Why | Failure |
250
+ |---|---|---|
251
+ | `orderBy` on a total order | the matcher decides *enters / leaves / moves within* the result from the changed row alone | `x verify` error naming the query |
252
+ | `limit` | an unbounded result set has no bounded change buffer and no bounded reconnect snapshot | `x verify` error naming the query |
253
+ | No `now()`, `random()`, or non-deterministic function | the same `(input, row)` must always yield the same membership answer | `x verify` error naming the expression |
254
+ | No cross-tenant predicate | tenant scoping comes from `ctx`, not from `input` | `X_FORBIDDEN` at subscribe |
255
+ | The source reads as its subscriber's tenant | a sync node shares one read per `(query, input)` **per org**, in a context carrying that org — so `repo.list(limit)` needs no org argument on a live query, exactly as on a request | a source naming another org is `X_TENANCY_ACTOR_MISMATCH`; a subscriber with no org reading a tenant table is `X_TENANCY_ACTOR_ORG_REQUIRED` |
256
+
257
+ **A patched row lands where `order by` returns it** (`As of 2026-10`). The matcher, `from()`'s in-memory execution and the cursor fallback compare two values by the column's DECLARED kind, through `@ultimat3/entity`'s own comparator — `@ultimat3/query` has none. A `bigint()` or `decimal()` column orders by its digits (its row value is decimal text: `'9'` before `'10'`), a `uuid()` is a value whatever its case, a `timestamp()` is an instant. The kinds resolve from the entity declared on the table `from(name, …)` names; a relation no entity declares has none, and its digits compare as text — as a `text` column's do. A custom `SqlSource` calling `compareRows`, `matchesFilter` or `isAfterKey` passes `kindsOf(shape.entity)`; `compareValues` is gone.
258
+
259
+ A page is served `order by <declared keys>, "id" asc`, and so is a live window — `As of 2026-08`,
260
+ the initial window, the matcher's patch positions and the keyset re-read a reconnect resumes with
261
+ are one ordering. A row tied on every declared key lands where its id puts it, not after the tie
262
+ group, and not wherever the database happened to return it. `x queries show <name> --json`
263
+ prints the order. A row that reaches the matcher with no `id` is `X_QUERY_NOT_PAGEABLE`, never a
264
+ patch aimed at a position no client holds.
265
+
266
+ A non-live query has none of these constraints — it is just a read.
267
+
268
+ ## A sealed column never rides on a row
269
+
270
+ Nothing parses a query's row on its way out: a declared `rows:` is published as the row's structure, never applied to the row. A `.sealed()` column
271
+ stays behind because the repository row does not enumerate it
272
+ ([Sealed columns](Entities-And-Migrations#sealed-columns)), `As of 2026-10`.
273
+
274
+ | Projection | Sealed column |
275
+ |---|---|
276
+ | `GET /_x/query/<name>` — list, `Page`, `single: true`, the record envelope's `data` and `records` | absent |
277
+ | the MCP read tool, a `cache:` entry in a shared tier | absent |
278
+ | a live snapshot and every patch | absent |
279
+ | `query(input)` / `.as(actor, input)` called on the server | `row.password` reads the plaintext |
280
+ | a derived row — `rows.map((row) => ({ ...row, initials }))` | absent; the spread drops it |
281
+ | a derived row that names it — `({ id: row.id, token: row.token })` | **sent.** The only way to send a secret is to write its name |
282
+
283
+ | Refused | Code |
284
+ |---|---|
285
+ | `live: true` with a sealed column in `.where()` / `.compare()` / `.orderBy()` | `X_MATCHER_UNSUPPORTED` at subscribe — a change row carries no sealed column, so the window could be read once and never patched |
286
+
287
+ ## Row-level policy filtering
288
+
289
+ Policy is not a subscribe-time gate that then trusts the stream.
290
+
291
+ | Moment | Check |
292
+ |---|---|
293
+ | Subscribe | `policy` evaluated against `{ input, actor }`. Denied → `X_FORBIDDEN`, no subscription created |
294
+ | Initial snapshot | every row filtered through the same policy |
295
+ | Each incremental patch | re-checked per row. A row that fails is **dropped, never sent** |
296
+ | Actor change (role revoked, org left) | the subscription re-evaluates; rows that no longer pass are delivered as `delete` ops |
297
+ | Actor moves to another org | the subscription is re-seated on that org's own window and sent its snapshot: a window is one tenant's, and two orgs never share one |
298
+ | A refused subscription, on the page | `useQuery`'s `failed` state carries the node's error as a branded `UltimateError` — its code, cause and fix — never `X_INTERNAL` |
299
+ | Topic guards (tier 1) | `X_TOPIC_FORBIDDEN` — cause names the actor and topic, never the topic's data |
300
+
301
+ One authz system. A live query cannot become a second door into your data — that is the failure mode that killed the `allow`/`deny` generation of frameworks ([The eight primitives](The-Eight-Primitives)).
302
+
303
+ ## NULL
304
+
305
+ `As of 2026-08`, one rule for the generated SQL, the in-memory source behind `from()` and the live
306
+ matcher alike. `null` and a column the row omits are the same absence.
307
+
308
+ | Operator | NULL is | Generated SQL |
309
+ |---|---|---|
310
+ | `=` `!=` `in` | a value — it matches itself and nothing else | `is null` · `is not null` · `is distinct from` · `in (…) or is null` |
311
+ | `>` `>=` `<` `<=` | unknown — a NULL on either side matches nothing | `"col" > $n`, which already matches no NULL |
312
+ | `orderBy`, the cursor | the largest value: last ascending, first descending | `asc nulls last` · `desc nulls first` |
313
+
314
+ `where({ deletedAt: null })` emits `"deletedAt" is null` and binds no parameter. `= $1` with a NULL
315
+ argument is *unknown* in Postgres and unknown is never true, so before this the same read matched
316
+ every row from a memory source and no row from a driver. A cursor across a nullable sort key had
317
+ the defect one page later: page two stopped at the first NULL, and the rows behind it were
318
+ unreachable. `x queries show <name> --json` prints the SQL that says so.
319
+
320
+ **Both pagination systems now answer this the same way, `As of 2026-08-24`.** `@ultimat3/entity`'s
321
+ repo cursor used to refuse a nullable sort key outright rather than answer where a NULL sorts; it
322
+ now writes the same two orderings down (`asc nulls last` / `desc nulls first`), carries the absence
323
+ in the cursor and reaches it in the seek. One answer, per axiom 1. What entity still refuses is
324
+ narrower and is not about ordering: a **nullable primary-key column** used as the tiebreak —
325
+ `null = null` is unknown, so two rows sharing a sort value are indistinguishable to the seek and one
326
+ of them is served twice or never ([Entities and migrations](Entities-And-Migrations)).
327
+
328
+ ## Request memo (tier 1) dedupe
329
+
330
+ Three components on one page calling `liveFeed({ orgId })` resolve **one** query.
331
+
332
+ | Property | Behavior |
333
+ |---|---|
334
+ | Coverage | every query, `cache:` or not. `cache:` buys the tier behind the memo, never the memo itself |
335
+ | Store | a map per request context, keyed by query name + `input` fingerprint + the query's tags |
336
+ | Lifetime | one request. No cross-request reuse, no eviction policy to tune |
337
+ | Hit cost | ~0 |
338
+ | Concurrency | the entry is the read *in flight*, so a caller that races the first one joins it instead of starting a competing read |
339
+ | Scope safety | two actors never share an entry — each request has its own context, and `.as()` reads in a child of it |
340
+ | Authorization | parsing, the policy and `sql` run on every call. What the memo holds is the execution, never the decision |
341
+ | Failure | a rejection is evicted, so the next read in the request retries rather than replaying one failure |
342
+ | Invalidation | none — a write in the same request drops tier entries, not memo entries. `fresh: true` is the read-past, and what it read replaces the memo entry, so the next plain read of that key sees the write too |
343
+
344
+ Streamed `<Hole>`s ([Routes and render modes](Routes-And-Render-Modes)) are the common case: independent holes, one round trip to Postgres. The same memo is what keeps an uncached lookup called once per row of a list to one round trip — `As of 2026-08`, per row is what it used to cost.
345
+
346
+ The memo collapses the *same* read asked twice. Fifty *different* row lookups collapse one layer down, in the repo: `findById` issued across one microtask of a request is one `where "id" in (…)` ([Entities and migrations → Point lookups batch themselves](Entities-And-Migrations#point-lookups-batch-themselves)) — or, named on the chain instead of inferred from a loop, one `preload()` per relation ([Entities and migrations → Preload states a relation the loop would infer](Entities-And-Migrations#preload-states-a-relation-the-loop-would-infer)).
347
+
348
+ Fifty *counts* collapse neither way — one `count()` per row is fifty different questions, so nothing above the repo can batch them. The repo answers them in one statement instead: `db.likes.where({ orgId }).andWhere('postId', 'in', ids).countBy('postId')` is a map keyed by the column's values, biggest group first, with a value nothing matched absent rather than `0` ([Entities and migrations → A count per row is one grouped count](Entities-And-Migrations#a-count-per-row-is-one-grouped-count)).
349
+
350
+ ## Audit: who saw what
351
+
352
+ `query({ audit: true })` writes one record per call into the sink an action's `audit: true` writes to — one contract, `@ultimat3/core`'s `AuditRecord` / `AuditSink`, one installed sink (`setAuditSink` from `@ultimat3/core` — `@ultimat3/action` no longer re-exports it, `As of 25.0.0`). `As of 2026-10`.
353
+
354
+ | Rule | |
355
+ |---|---|
356
+ | What is a call | `read(input)`, `.as()`, the HTTP GET, `.page()` — one record each. `sourceFor(…)` + `execute()` (how `@ultimat3/mcp` serves a tool): one per `execute()`, or one at the build when it is refused |
357
+ | A memo or cache hit | **recorded**, `replayed: true` — an audit of a read is who saw what, not what the database computed |
358
+ | Outcomes | `allowed` · `denied` (a policy refusal, before or after the input parse) · `failed` (an unparsed input, a spent rate limit, a throwing source) |
359
+ | `surface` | `server` · `http` · `mcp` — the same vocabulary an action's record uses |
360
+ | `name` | the export name. `action` holds the same value, **deprecated**, removed in 25.0.0 — read `name` |
361
+ | `primitive` | `'query'` (an action's record says `'action'`) — what tells a read from a write in one sink |
362
+ | `input` | the parsed input, never the raw payload; a persisting sink redacts it through `auditableInput` |
363
+ | rows | **never on the record**, as an action's result never is |
364
+ | Not recorded | `explainQuery()`, `describeSql()`, the shared live window: built `unenforced`, no caller to attribute |
365
+ | No sink installed | `X_QUERY_AUDIT_SINK_MISSING`, before the input parse |
366
+ | The sink refuses | a denied/failed record: logged (`audit.sink.failed`), the caller gets the original error. An allowed record: the rows are withheld, `X_QUERY_AUDIT_SINK_FAILED` — a read commits nothing, so retry |
367
+
368
+ ## Every cached query carries a tag
369
+
370
+ The contract. **Not yet a gate** — `As of 2026-08` `X_CACHE_UNTAGGED_QUERY` is reserved: no code path raises it, and `x errors explain X_CACHE_UNTAGGED_QUERY` refuses it ([Error codes → Reserved codes](Error-Codes#reserved-codes)).
371
+
372
+ | Case | Today |
373
+ |---|---|
374
+ | a cached query no tag covers | cached under a key no `invalidates` fan-out reaches — stale until its `ttlMs`, and forever without one |
375
+ | a tag no entity declares | `X_CACHE_TAG_UNKNOWN`, `fix: x manifest` — the opposite mistake, and the one that is enforced |
376
+ | the `x verify` gate | no step reads a query's tags. Nothing fails |
377
+
378
+ Until it is a gate, tag coverage is a review item, not a build error. Declare the entity tag — `tags()` / `entityTag()` in [Caching and invalidation](Caching-And-Invalidation).
379
+
380
+ ## Subscription caps
381
+
382
+ Load shedding is a decision with a typed error, not a fall-over.
383
+
384
+ | Code | Trigger | Fix |
385
+ |---|---|---|
386
+ | `X_SUBSCRIPTION_LIMIT` | a socket, tenant or node reached a cap; the error names which scope refused, and which knob | `raise maxPerSocket / maxPerTenant / maxEntries on the LiveQueryRegistry (per socket, default 128), or unsubscribe unused live queries` — and a **channel topic** cap answers `maxTopicsPerSocket` / `maxTopicsPerNode` on the `ChannelHub`. All constructor options, none an `app.config.ts` field |
387
+ | `X_CURSOR_STALE` | resume cursor outside the change buffer and no snapshot path supplied | `pass 'snapshot' to resumeFrom() so the fallback path can re-snapshot instead of failing` |
388
+ | `X_TRANSPORT_UNAVAILABLE` | the fanout bus is unreachable | `x doctor transport` |
389
+
390
+ Verbatim shapes: [`packages/realtime/src/errors.ts`](https://github.com/developerz-ai/ultimate/blob/main/packages/realtime/src/errors.ts). Full index: [Error codes](Error-Codes).
391
+
392
+ ## Testing
393
+
394
+ `x test live` — its own runner, its own fixture shape. Runs against a cloned Postgres plus an in-process replicator and in-process NATS.
395
+
396
+ | Asserted | Why it catches regressions |
397
+ |---|---|
398
+ | Initial snapshot | the `sql` is right, ordered, and bounded |
399
+ | Incremental patch on write | the matcher's enter/leave/update decision is right for the changed row |
400
+ | Reconnect delta | resume from an LSN produces the same state as a fresh snapshot |
401
+ | Policy-filtered row never delivered | the per-row re-check actually runs |
402
+
403
+ Every `query({ live: true })` emits a test covering snapshot + one patch + one policy-filtered row, green on the first run. Extend it as the query grows — an untested live query is a red build.
404
+
405
+ ```
406
+ x test live --json
407
+ x verify # runs all six test types
408
+ ```
409
+
410
+ ## Introspection
411
+
412
+ | Command | Output |
413
+ |---|---|
414
+ | `x queries list --json` | `name`, `live`, `capability`, `tags`, `ttlMs` — the table header is `cmd-registries.ts`'s own |
415
+ | `x queries show <name> --json` | generated SQL, tag set, MCP tool shape |
416
+ | `x cache graph --json` | what a write to each tag evicts, including this query's entry |
417
+
418
+ ## Rules
419
+
420
+ - One query per read shape. Two queries differing by a boolean is one query with a boolean `input` field.
421
+ - Filter rows in `sql`; decide yes/no in `policy`. Never mix.
422
+ - `live: true` needs `orderBy` + `limit`, always.
423
+ - Presence, typing indicators, and cursors are tier 1 channels forever — never model ephemeral state as rows ([Realtime](Realtime)).
424
+ - A cache miss must be correct and merely slower. No query may depend on a hit.
425
+ - NULL is a value to `=`/`!=`/`in`, unknown to `>`/`<`, and the largest value to `orderBy`. Never
426
+ write a filter that reads a NULL a fourth way.
427
+ - One order, three readers: the generated SQL, the cursor and the live matcher all serve
428
+ `<declared keys>, "id" asc`. Never sort a window by the declared keys alone.
429
+ - Cache keys are framework-generated. A hand-built key is a rejected PR.