@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
@@ -4,11 +4,11 @@
4
4
 
5
5
  import { logger } from '@ultimat3/core';
6
6
  import type { RedirectIntent, RequestContext, RouteParams } from '@ultimat3/http';
7
- import { asCtx, requestAddress, takeRedirect } from '@ultimat3/http';
7
+ import { asCtx, requestAddress, takeRedirect, withAnonymousRequest } from '@ultimat3/http';
8
8
  import type { RenderResult, RouteData, RouteEntry } from '@ultimat3/render';
9
9
  import { describePages, routeDataFor, routeNoStoreOf, routeStatusOf } from '@ultimat3/render';
10
10
  import type { IsrController, IsrEntry } from '@ultimat3/render/server';
11
- import { isrKey, isrRequestUrl, isrRouteFor, undeclaredQuery } from '@ultimat3/render/server';
11
+ import { isrKey, isrRequestUrl, isrRouteFor } from '@ultimat3/render/server';
12
12
 
13
13
  export type IsrOutcome =
14
14
  | { readonly kind: 'page'; readonly result: RenderResult }
@@ -23,47 +23,83 @@ export interface IsrRequest {
23
23
  readonly data: IsrData;
24
24
  readonly ctx: RequestContext;
25
25
  readonly isr: IsrController;
26
+ /** The app's declared public origin (`publicOrigin()`), or nothing: then the request's is keyed. */
27
+ readonly origin: string | undefined;
26
28
  /**
27
- * The route's whole document for what `load` returned (`runtime-render.ts`'s `documentFrom`),
29
+ * The route's whole document for what `load` returned (`runtime-render-document.ts`'s `documentFrom`),
28
30
  * rendered AT the data this file hands it — the narrowed URL, never the request's own.
29
31
  */
30
32
  readonly document: (loaded: RouteData, at: IsrData) => Promise<string>;
31
33
  }
32
34
 
33
- /** Route files already told, once, about a request whose query they never declared. */
34
- const toldUndeclared = new Set<string>();
35
-
36
- /** Names are the visitor's: a few, cut short, is all a log line needs to name the edit. */
37
- const NAMED_PARAMS = 5;
38
- const NAMED_PARAM_CHARS = 40;
39
-
40
35
  /**
41
36
  * The request narrowed to what the route declared it varies on (`revalidate.query`), AT the one
42
37
  * spelling of its address (`pathname`). ONE narrowing feeds the store key, `load`, `meta` and the
43
38
  * page, so neither a parameter outside the key nor the way a visitor happened to spell the path
44
39
  * (`/blog//a/`, `/blog/%61`) can reach the document stored under it.
45
40
  *
46
- * A route that declared NOTHING still keys on the whole query, and is told so once, the first time
47
- * a visitor uses that: every distinct query string is then a render and a stored page of its own.
41
+ * The origin is the same narrowing: a declared one replaces the request's before the key, so `load`
42
+ * and the page are handed the address the document's own canonical names, whatever `Host` arrived.
48
43
  */
49
- function keyedData(entry: RouteEntry, data: IsrData, pathname: string | undefined): IsrData {
44
+ function keyedData(
45
+ entry: RouteEntry,
46
+ data: IsrData,
47
+ pathname: string | undefined,
48
+ origin: URL | undefined,
49
+ ): IsrData {
50
50
  const url = new URL(data.url);
51
51
  if (pathname !== undefined) url.pathname = pathname;
52
- const declared = entry.config.revalidate?.query ?? null;
53
- if (declared === null && !toldUndeclared.has(entry.file)) {
54
- const minted = undeclaredQuery(url, declared);
55
- if (minted.length > 0) {
56
- toldUndeclared.add(entry.file);
57
- logger.warn('isr.query.undeclared', {
58
- route: entry.file,
59
- params: minted.slice(0, NAMED_PARAMS).map((name) => name.slice(0, NAMED_PARAM_CHARS)),
60
- cause:
61
- 'the route declares no revalidate.query, so every distinct query string is rendered and stored as its own page',
62
- fix: `revalidate: { query: [] } // in ${entry.file}; or query: ['<name>'] for each parameter the page varies on`,
63
- });
64
- }
52
+ if (origin !== undefined) {
53
+ url.protocol = origin.protocol;
54
+ url.hostname = origin.hostname;
55
+ // `port` and not `host`: a `host` with no port leaves the request's, so `evil.test:8443` named
56
+ // itself in the document after all.
57
+ url.port = origin.port;
65
58
  }
66
- return { ...data, url: isrRequestUrl(url, declared).href };
59
+ return { ...data, url: isrRequestUrl(url, entry.config.revalidate?.query ?? null).href };
60
+ }
61
+
62
+ /**
63
+ * The declared origin, when it is one: http or https. `APP_URL` is an operator's string and nothing
64
+ * has read it as a URL by here. One that does not parse would be a 500 on every `isr` page, and
65
+ * `localhost:3000` parses with `localhost:` as its scheme, where URL setters do nothing and every
66
+ * host would share one key. Both are keyed as undeclared: against the request, as the head falls back.
67
+ */
68
+ function declaredOrigin(origin: string | undefined): URL | undefined {
69
+ if (origin === undefined || !URL.canParse(origin)) return undefined;
70
+ const parsed = new URL(origin);
71
+ return parsed.protocol === 'http:' || parsed.protocol === 'https:' ? parsed : undefined;
72
+ }
73
+
74
+ /**
75
+ * How many request origins one controller stores pages for when the app declares none. A `Host` is
76
+ * the visitor's to choose, so each new one would be a render and an entry pushing a real page out
77
+ * of the bounded store. Past the cap a NEW origin is refused rather than anything evicted, as
78
+ * `static-document.ts`'s memo does: answered, its own canonical, never stored.
79
+ */
80
+ export const ISR_MAX_REQUEST_ORIGINS = 16;
81
+
82
+ /** Per controller, so a `stop` that drops the store drops what it had admitted. */
83
+ const admittedOrigins = new WeakMap<IsrController, Set<string>>();
84
+
85
+ function admitsOrigin(isr: IsrController, origin: string, route: string): boolean {
86
+ const admitted = admittedOrigins.get(isr) ?? new Set<string>();
87
+ admittedOrigins.set(isr, admitted);
88
+ if (admitted.has(origin)) return true;
89
+ if (admitted.size < ISR_MAX_REQUEST_ORIGINS) {
90
+ admitted.add(origin);
91
+ return true;
92
+ }
93
+ if (admitted.size === ISR_MAX_REQUEST_ORIGINS) {
94
+ // Told once: a sentinel past the cap, which nothing else ever reads as an origin.
95
+ admitted.add('');
96
+ logger.warn('isr.origin.over_cap', {
97
+ route,
98
+ cause: `this process has stored pages for ${ISR_MAX_REQUEST_ORIGINS} request origins and the app declares none, so a page for any other Host is answered uncached`,
99
+ fix: 'APP_URL=https://your.domain # or site: { origin } in app.config.ts',
100
+ });
101
+ }
102
+ return false;
67
103
  }
68
104
 
69
105
  /**
@@ -94,50 +130,61 @@ function dropPage(isr: IsrController, key: string): void {
94
130
  * inside the producer, which runs on a miss and on a stale page's regeneration.
95
131
  *
96
132
  * Concurrent misses on one key share ONE producer run, and only a PAGE may be shared. A redirect is
97
- * the request's own (its `load` may have read a cookie), so a request that joined a run whose load
133
+ * never stored, so it stays the decision of the render that made it: a request that joined a run whose load
98
134
  * redirected decides for itself, exactly as the static single-flight in `appRoutes` does.
135
+ *
136
+ * `load` and the document run as nobody, on every path (`withAnonymousRequest`): what is stored is
137
+ * answered to every later visitor, so it may not hold what this one's identity could read.
99
138
  */
100
139
  export async function isrOutcome(arrived: IsrRequest): Promise<IsrOutcome> {
101
140
  // Where the router MATCHED this request, never where the visitor wrote it: the router answers
102
141
  // many spellings with one page, and a key built from the raw pathname stored each of them.
103
142
  const address = requestAddress(arrived.ctx);
143
+ const origin = declaredOrigin(arrived.origin);
104
144
  const request: IsrRequest = {
105
145
  ...arrived,
106
- data: keyedData(arrived.entry, arrived.data, address?.pathname),
146
+ data: keyedData(arrived.entry, arrived.data, address?.pathname, origin),
107
147
  };
108
148
  const { entry, data, ctx, isr } = request;
109
- // `isrKey(url, locale)`, never `url.pathname`: the DECLARED query is part of what was rendered —
149
+ // `isrKey(url, locale, origin)`, never `url.pathname`: the DECLARED query is part of what was rendered —
110
150
  // `meta` reads `data.url` — so two URLs differing in it are two documents (#171). The locale is
111
151
  // `ctx.locale`, the one the `locale` stage negotiated for THIS request. The key's path carries
112
152
  // NO base path: the route table has never heard of the mount, so a key that kept it matched no
113
153
  // route — and every `isr` page of an app under a `basePath` was stored with no ttl and no tags.
114
154
  const keyed = new URL(data.url);
115
155
  if (address !== undefined) keyed.pathname = address.path;
116
- const key = isrKey(keyed, asCtx(ctx).locale);
156
+ // No origin declared: the document's canonical is absolute against the request's, so a foreign
157
+ // `Host` is its own entry and never names itself in the page the next visitor gets.
158
+ const key = isrKey(keyed, asCtx(ctx).locale, origin === undefined ? keyed.origin : undefined);
117
159
  // The key has to read back as THIS route, or its ttl, its tags and what a bust does to it are
118
160
  // another route's or nobody's. It always does for an address the framework's URL scheme can
119
161
  // spell; what is left is a path the router matched that another reading claims (`//en/x` is the
120
162
  // `/:lang/x` route with no locale, and `/en/x` is the `/x` route in English). Answered, uncached.
121
163
  if (isrRouteFor(describePages(), key)?.file !== entry.file) return ownOutcome(request);
164
+ if (origin === undefined && !admitsOrigin(isr, keyed.origin, entry.file)) {
165
+ return ownOutcome(request);
166
+ }
122
167
  let decided: RedirectIntent | undefined;
123
168
  // `{ html, status }`, never the bare string: the entry stores the status beside the HTML and
124
169
  // serves it on every hit, so a 404 under `isr` is a 404 for its whole TTL.
125
- const served = await isr.serve(key, async () => {
126
- const loaded = await routeDataFor(entry.config, data);
127
- const to = takeRedirect(ctx);
128
- if (to === undefined) {
129
- return {
130
- html: await request.document(loaded, data),
131
- status: routeStatusOf(loaded),
132
- noStore: routeNoStoreOf(loaded),
133
- };
134
- }
135
- // Set by THIS request's producer only. On a stale page it runs behind the answer, and when it
136
- // has already decided by the time `serve` returns, the redirect is still this request's own.
137
- decided = to;
138
- dropPage(isr, key);
139
- return { html: '', status: to.status };
140
- });
170
+ const served = await isr.serve(key, () =>
171
+ withAnonymousRequest(ctx, new URL(data.url), async (scope) => {
172
+ const loaded = await routeDataFor(entry.config, data);
173
+ const to = takeRedirect(scope);
174
+ if (to === undefined) {
175
+ return {
176
+ html: await request.document(loaded, data),
177
+ status: routeStatusOf(loaded),
178
+ noStore: routeNoStoreOf(loaded),
179
+ };
180
+ }
181
+ // Set by THIS request's producer only. On a stale page it runs behind the answer, and when it
182
+ // has already decided by the time `serve` returns, the redirect is still this request's own.
183
+ decided = to;
184
+ dropPage(isr, key);
185
+ return { html: '', status: to.status };
186
+ }),
187
+ );
141
188
  if (decided !== undefined) return { kind: 'redirect', to: decided };
142
189
  if (!isRedirectMarker(served.entry)) return { kind: 'page', result: served.result };
143
190
  return ownOutcome(request);
@@ -146,19 +193,26 @@ export async function isrOutcome(arrived: IsrRequest): Promise<IsrOutcome> {
146
193
  /**
147
194
  * A request answered for itself alone: a joiner whose leader's load redirected, or one whose key
148
195
  * is not its own route's. Its own `load`; a page here is NOT stored — the store was just told the
149
- * key has none, or the key is another page's — so it is `private, no-store`.
196
+ * key has none, or the key is another page's — so it is `private, no-store`. Still as nobody: not
197
+ * to protect the store, but because one route renders one way, whichever path a request took.
150
198
  */
151
- async function ownOutcome(request: IsrRequest): Promise<IsrOutcome> {
152
- const loaded = await routeDataFor(request.entry.config, request.data);
153
- const to = takeRedirect(request.ctx);
154
- if (to !== undefined) return { kind: 'redirect', to };
155
- const body = await request.document(loaded, request.data);
156
- return {
157
- kind: 'page',
158
- result: {
159
- status: routeStatusOf(loaded),
160
- headers: { 'cache-control': 'private, no-store' },
161
- body,
199
+ function ownOutcome(request: IsrRequest): Promise<IsrOutcome> {
200
+ return withAnonymousRequest(
201
+ request.ctx,
202
+ new URL(request.data.url),
203
+ async (scope): Promise<IsrOutcome> => {
204
+ const loaded = await routeDataFor(request.entry.config, request.data);
205
+ const to = takeRedirect(scope);
206
+ if (to !== undefined) return { kind: 'redirect', to };
207
+ const body = await request.document(loaded, request.data);
208
+ return {
209
+ kind: 'page',
210
+ result: {
211
+ status: routeStatusOf(loaded),
212
+ headers: { 'cache-control': 'private, no-store' },
213
+ body,
214
+ },
215
+ };
162
216
  },
163
- };
217
+ );
164
218
  }
@@ -1,8 +1,8 @@
1
1
  // The retention sweep this boot owns: the framework tables that grow with traffic, the `purge()`
2
2
  // job that empties them and the `task` that fires it hourly.
3
3
  //
4
- // NINE TARGETS, `As of 2026-10-17` — `x_idempotency`, `x_rate_limit`, the `x_auth` limiter's
5
- // table, `x_notify_deliveries`, `x_notify_inbox`, `x_notify_digests`, `x_job_events`, `x_outbox`
4
+ // TEN TARGETS, `As of 2026-11-02` — `x_idempotency`, `x_rate_limit`, the `x_auth` limiter's
5
+ // table, `x_totp_steps`, `x_notify_deliveries`, `x_notify_inbox`, `x_notify_digests`, `x_job_events`, `x_outbox`
6
6
  // (published rows) and `x_jobs` (finished rows, with their `x_job_steps`). Counted nowhere in prose but here: this header said
7
7
  // "three" for two releases after the notify tables joined the boot's DDL, which is how
8
8
  // `x_notify_inbox` became the one framework table nothing swept — and `x_jobs` was the next one.
@@ -19,6 +19,7 @@
19
19
  // per source address forever.
20
20
 
21
21
  import type { PostgresIdempotencyStore } from '@ultimat3/action';
22
+ import type { PostgresTotpReplayGuard } from '@ultimat3/auth';
22
23
  import { purgeAuthLimits } from '@ultimat3/auth';
23
24
  import type { PostgresRateLimitStore } from '@ultimat3/http';
24
25
  import type { EventBus, JobHandle, PurgeInput, PurgeTarget } from '@ultimat3/jobs';
@@ -52,6 +53,12 @@ export const PURGE_TASK_NAME = 'x.purge.hourly';
52
53
  export interface RetentionStores {
53
54
  readonly idempotency: PostgresIdempotencyStore;
54
55
  readonly rateLimit: PostgresRateLimitStore;
56
+ /**
57
+ * The guard this boot installed through `configureTotpReplay`. Unlike the limiter it takes
58
+ * nothing from the app, so it exists here. REQUIRED, or `x_totp_steps` keeps a row per second
59
+ * factor for the life of the deployment.
60
+ */
61
+ readonly totpReplay: PostgresTotpReplayGuard;
55
62
  /**
56
63
  * The bus this boot installed for `step.waitForEvent`. REQUIRED: `x_job_events` takes a row per
57
64
  * publish and `find` only filters an expired one, so a boot that left it out is the table
@@ -149,6 +156,11 @@ function retentionTargets(stores: RetentionStores): readonly PurgeTarget[] {
149
156
  purgeExpired: (nowMs: number): Promise<number> => stores.rateLimit.purgeExpired(nowMs),
150
157
  },
151
158
  authTarget,
159
+ {
160
+ name: 'x_totp_steps',
161
+ // The job's clock: `verifyTotp` derived every step in that table from a process's clock.
162
+ purgeExpired: (nowMs: number): Promise<number> => stores.totpReplay.purgeExpired(nowMs),
163
+ },
152
164
  ...notifyTargets(stores.inboxRetention ?? NO_INBOX_RETENTION),
153
165
  // Its own clock, and so no `nowMs`: every instant in that table is the database's.
154
166
  eventsPurgeTarget(stores.events),
@@ -7,9 +7,12 @@
7
7
  import {
8
8
  type PostgresIdempotencyStore,
9
9
  postgresIdempotencyStore,
10
+ postgresMeterStore,
10
11
  requestDeadlineMs,
11
12
  resetIdempotency,
13
+ resetMeterStore,
12
14
  setIdempotencyStore,
15
+ setMeterStore,
13
16
  } from '@ultimat3/action';
14
17
  import type { PgliteClient, PostgresClient } from '@ultimat3/db';
15
18
  import {
@@ -18,6 +21,8 @@ import {
18
21
  dbExecutor,
19
22
  pgliteClient,
20
23
  pgliteDataDir,
24
+ poolerFromEnv,
25
+ poolerSessionRequired,
21
26
  poolMaxFromEnv,
22
27
  postgresClient,
23
28
  setDbClient,
@@ -111,7 +116,7 @@ export function startDb(services: DevServices, env: ReplicaEnv): StartedDb {
111
116
  dataDir: pgliteDataDir(binding.url),
112
117
  extensions: () => appExtensions(services.root),
113
118
  })
114
- : postgresClient({ url: binding.url, profile });
119
+ : postgresClient({ url: binding.url, profile, env });
115
120
  const attached = attachReplica(client, replicaUrlFor(binding, env), profile);
116
121
  setDbClient(attached.client);
117
122
  return { client, replica: attached.replica };
@@ -182,8 +187,8 @@ async function startJobs(
182
187
  schema: SchemaMode,
183
188
  ): Promise<RunningQueue> {
184
189
  await applySchema(client, schema);
185
- // Bound to `client`, never the ambient `db()`: the queue, the event bus and the idempotency
186
- // reservations stay on the boot's pool whatever transaction is open.
190
+ // Bound to `client`, never the ambient `db()`: the queue and the idempotency reservations stay
191
+ // on the boot's pool whatever transaction is open.
187
192
  const executor = dbExecutor(() => client);
188
193
  const driver = overrides?.jobs ?? postgresJobDriver({ executor });
189
194
  setJobDriver(driver);
@@ -195,7 +200,16 @@ async function startJobs(
195
200
  setJobsFacade(
196
201
  outboxJobsFacade({ store: outbox, driver }, () => currentTx() as unknown as Tx | undefined),
197
202
  );
198
- const events = postgresEventBus({ executor });
203
+ // The open transaction's connection, as the outbox beside it: an answer belongs to the unit of
204
+ // work that gave it, so a refusal that rolls back never resumes the run waiting on it.
205
+ //
206
+ // `wakes` only when the queue in hand is the boot's own `postgresJobDriver`: the wake is an update
207
+ // of `x_jobs` on this database, and an override driver (Redis) keeps no rows here — its waiters
208
+ // keep the 30 s poll. Waking, a parked wait's backstop is 15 min instead.
209
+ const events = postgresEventBus({
210
+ executor: dbExecutor(() => currentTx() ?? client),
211
+ wakes: overrides?.jobs === undefined,
212
+ });
199
213
  setEventBus(events);
200
214
  const idempotency = postgresIdempotencyStore({
201
215
  executor,
@@ -206,6 +220,9 @@ async function startJobs(
206
220
  reclaimAfterMs: requestDeadlineMs,
207
221
  });
208
222
  setIdempotencyStore(idempotency);
223
+ // `meter:` counters on the boot's pool, never the open transaction: a draw is the caller's
224
+ // whether or not the business write commits, and one tenant's day is one row on every replica.
225
+ setMeterStore(postgresMeterStore({ executor }));
209
226
  return {
210
227
  db: client,
211
228
  jobs: driver,
@@ -234,6 +251,7 @@ async function releaseQueue(
234
251
  // The idempotency store goes back to the memory default for the same reason the facade does: it
235
252
  // holds this client, and the next command in this process would reserve keys over a closed one.
236
253
  resetIdempotency();
254
+ resetMeterStore();
237
255
  resetJobsFacade();
238
256
  resetEventBus();
239
257
  resetJobDriver();
@@ -266,6 +284,15 @@ export async function startQueue(
266
284
  */
267
285
  schema: SchemaMode = 'ensure',
268
286
  ): Promise<RunningQueue> {
287
+ // `x db migrate` runs as whatever ROLE the shell has, so `postgresClient`'s role check cannot see
288
+ // it: what needs the session is the step that takes the migration lock, whichever role asked.
289
+ if (
290
+ schema === 'apply' &&
291
+ services.db.mode !== 'embedded' &&
292
+ poolerFromEnv(env) === 'transaction'
293
+ ) {
294
+ throw poolerSessionRequired('a step that applies migrations');
295
+ }
269
296
  const { client: db, replica } = startDb(services, env);
270
297
  try {
271
298
  // Pay the Postgres boot here, so the first request is not the slow one and a broken database
@@ -0,0 +1,204 @@
1
+ // One route's document: the head a route and the app decide together, the body inside the
2
+ // hydration root, and the assembly of the two. Split from `runtime-render.ts`, which owns picking a
3
+ // mode and answering with what it rendered; every mode's document is built here, once.
4
+
5
+ import { actionPathStyle } from '@ultimat3/action';
6
+ import { escapeHtml } from '@ultimat3/core';
7
+ import type { RouteParams } from '@ultimat3/http';
8
+ import { currentLocale } from '@ultimat3/i18n';
9
+ import type { IslandCollector, RouteData, RouteEntry } from '@ultimat3/render';
10
+ import {
11
+ clientPathStyleTags,
12
+ clientPersistTags,
13
+ clientPinTags,
14
+ clientScopeTag,
15
+ clientSyncTags,
16
+ headFromMeta,
17
+ hydrateRuntime,
18
+ metaContextFor,
19
+ renderHead,
20
+ seoRenderers,
21
+ } from '@ultimat3/render';
22
+ import type { HoleSink } from '@ultimat3/render/server';
23
+ import { ROOT_ELEMENT_ID, renderComponent } from '@ultimat3/render/server';
24
+ import { loadAppConfig } from './app-config-load';
25
+ import type { DocumentOptions } from './document-options';
26
+ import { routeQuery } from './isr-query-guard';
27
+ import { navigationTagsOf } from './page-navigation';
28
+ import { servedPins } from './page-pins';
29
+ import { bootScript, collectorFor } from './route-islands';
30
+ import { siteAssetTable } from './site-assets';
31
+ import { styleBundle } from './style-bundle';
32
+
33
+ /**
34
+ * The app-wide favicon head, from `site.favicon`: the content-hashed URL `asset('assets/….svg')`
35
+ * returns and `/assets/*` serves, never the raw path. Per render, never frozen at boot.
36
+ */
37
+ export async function appFaviconHead(root: string): Promise<string | undefined> {
38
+ const favicon = (await loadAppConfig(root))?.site.favicon;
39
+ if (favicon === undefined) return undefined;
40
+ const url = siteAssetTable(root).resolve(favicon).url;
41
+ return `<link rel="icon" type="image/svg+xml" href="${escapeHtml(url)}">`;
42
+ }
43
+
44
+ /** `DocumentOptions`, plus the app root the app's own favicon is resolved against per render. */
45
+ export type RenderOptions = DocumentOptions & { readonly root?: string };
46
+
47
+ /** What a route's `meta(data)` is given. `url` is a string because that is what `ld.*` embeds. */
48
+ export interface DevRouteData extends Record<string, unknown> {
49
+ readonly url: string;
50
+ readonly params: RouteParams;
51
+ }
52
+ /**
53
+ * `<html lang>` is the request's own locale, never a constant: the `locale` stage negotiated it
54
+ * one stage before this handler and published it on the context, so a hardcoded `'en'` shipped
55
+ * every document mislabelled — wrong for a screen reader, wrong for `hreflang`. Outside a request
56
+ * (`x build`'s prerender) it is the app's own configured fallback.
57
+ */
58
+ export const lang = (): string => currentLocale();
59
+
60
+ /**
61
+ * `scope` is present only on a PRIVATE document (a gated `ssr` page, every `stream`): the page's
62
+ * client scope (`@ultimat3/auth`'s `clientScopeOf`), which core's `pageClient()` reads to fence its
63
+ * one store per principal. A shareable document carries NO scope tag — absent means "not rendered
64
+ * for anyone", a different answer from `''`, the anonymous page.
65
+ */
66
+ export const headFor = async (
67
+ entry: RouteEntry,
68
+ ctx: DevRouteData,
69
+ data: RouteData,
70
+ options: RenderOptions,
71
+ scope?: string,
72
+ ): Promise<string> => {
73
+ const meta = metaContextFor(ctx, data);
74
+ const url = new URL(ctx.url);
75
+ const router = navigationTagsOf(options.navigation, entry, meta.t);
76
+ return (
77
+ renderHead(
78
+ headFromMeta(
79
+ await entry.config.meta(meta),
80
+ seoRenderers({
81
+ path: url.pathname,
82
+ baseUrl: options.origin ?? url.origin,
83
+ // The default is the cluster's own head (`alternates` is default-first by contract), so
84
+ // `x-default` comes from the list the hreflang tags do — never a second locale read.
85
+ localization: {
86
+ locale: meta.locale,
87
+ defaultLocale: meta.alternates[0]?.locale ?? meta.locale,
88
+ alternates: meta.alternates,
89
+ },
90
+ }),
91
+ [
92
+ ...(options.sync === undefined ? [] : clientSyncTags(options.sync)),
93
+ // Every document, shareable ones included: the style is the app's, not a visitor's, and
94
+ // it is the only way an island's `rpc()` learns it. Read per render — `defineApi` may run
95
+ // after these routes are built. `'resource'`, the default, is no tag.
96
+ ...clientPathStyleTags(actionPathStyle()),
97
+ // Same reason, same read: a pinned action or query is reached by NAME from the browser
98
+ // (`rpc`, `useQuery`, `useMutation`) only through this. Nothing pinned is no tag.
99
+ ...clientPinTags(servedPins()),
100
+ ...router,
101
+ // The page boot rides the scope tag: its whole job — restoring a principal's persisted
102
+ // records and replaying its queued writes — is per principal, and a shareable document
103
+ // (no scope tag) has neither.
104
+ ...(scope === undefined
105
+ ? []
106
+ : [clientScopeTag(scope), ...clientPersistTags(options.persisted?.() ?? [])]),
107
+ ],
108
+ ),
109
+ ) +
110
+ (options.themeHead ?? '') +
111
+ // One mechanism per document: the router prefetches for itself, the browser for the rest.
112
+ (router.length === 0 ? (options.speculationHead ?? '') : '') +
113
+ // The app's own favicon: one link, every document, the URL the table resolves NOW.
114
+ ((options.root === undefined ? options.faviconHead : await appFaviconHead(options.root)) ??
115
+ '') +
116
+ // Per locale: each links its own locale's manifest (`PwaArtifacts.headFor`).
117
+ (typeof options.pwaHead === 'function'
118
+ ? options.pwaHead(meta.locale)
119
+ : (options.pwaHead ?? '')) +
120
+ (entry.surface === 'app' ? (options.appHead ?? '') : (options.siteHead ?? ''))
121
+ );
122
+ };
123
+
124
+ /**
125
+ * `<link rel="stylesheet">` for the surface's own stylesheets, or nothing at all when the surface
126
+ * imports none.
127
+ *
128
+ * In `<head>`, which is what keeps this a swap and not a regression: a `<link rel="stylesheet">`
129
+ * there is render-blocking in every browser, exactly as the inline block was, so there is no window
130
+ * in which the document paints unstyled. Moving it to the body, or deferring it, would introduce a
131
+ * flash — never do that here.
132
+ *
133
+ * Read through `styleBundle()` rather than passed in through `DocumentOptions`: the registry it
134
+ * derives from is process-global (importing the app IS what fills it), so a caller that forgot to
135
+ * thread a resolver would serve a document with no CSS at all. One reader, and it is the same one
136
+ * `styleRoutes` serves from.
137
+ */
138
+ const styleTag = (entry: RouteEntry): string => {
139
+ const href = styleBundle().hrefFor(entry.surface);
140
+ return href === undefined ? '' : `<link rel="stylesheet" href="${href}">`;
141
+ };
142
+
143
+ /** The surface stylesheet, then the app's brand — the order that lets the brand win the cascade. */
144
+ export const stylesFor = (entry: RouteEntry, options: DocumentOptions): string =>
145
+ styleTag(entry) + (options.brandHead ?? '');
146
+
147
+ /**
148
+ * The route's rendered body, inside the hydration root. Every mode goes through here — an empty
149
+ * root is what a module exporting no component renders, and nothing else: `spa` was the one mode
150
+ * that never reached this function, so every `spa` route ever declared served an empty root.
151
+ */
152
+ export async function routeBody(
153
+ entry: RouteEntry,
154
+ ctx: DevRouteData,
155
+ data: RouteData,
156
+ islands: IslandCollector,
157
+ // Only the `stream` branch passes one. Absent, a `<Hole>` is its children, inline.
158
+ holes?: HoleSink,
159
+ ): Promise<string> {
160
+ if (entry.component === undefined) return `<div id="${ROOT_ELEMENT_ID}"></div>`;
161
+ const url = new URL(ctx.url);
162
+ const html = await renderComponent(
163
+ entry.component,
164
+ // `data` is the route's own `load` result and is what `meta` was just given — the same object,
165
+ // never a second resolution. `query` is supplied because a page that reads `props.query.x`
166
+ // otherwise dereferences undefined and takes the whole render down.
167
+ {
168
+ data,
169
+ params: ctx.params,
170
+ url: ctx.url,
171
+ query: routeQuery(entry, url),
172
+ },
173
+ entry.file,
174
+ holes === undefined ? { islands } : { islands, holes },
175
+ );
176
+ return `<div id="${ROOT_ELEMENT_ID}">${html}</div>`;
177
+ }
178
+
179
+ /**
180
+ * The document from data ALREADY resolved. Split from `routeDocument` so one request resolves
181
+ * `load` exactly once: `stream` renders head and body separately, and resolving in each would let
182
+ * a `<title>` describe content the body does not contain.
183
+ *
184
+ * The hydration runtime is appended after the body and only after it: what strategies a page needs
185
+ * is a fact about the islands the walk just recorded, so emitting it earlier would guess or ship
186
+ * the whole runtime to a page with no island.
187
+ */
188
+ export async function documentFrom(
189
+ entry: RouteEntry,
190
+ ctx: DevRouteData,
191
+ data: RouteData,
192
+ options: RenderOptions,
193
+ scope?: string,
194
+ ): Promise<string> {
195
+ const islands = collectorFor(entry, options, scope);
196
+ const [head, body] = await Promise.all([
197
+ headFor(entry, ctx, data, options, scope),
198
+ routeBody(entry, ctx, data, islands),
199
+ ]);
200
+ return (
201
+ `<!doctype html><html lang="${lang()}"><head>${head}${stylesFor(entry, options)}</head>` +
202
+ `<body>${body}${bootScript(entry, islands, options, scope)}${hydrateRuntime(islands.directives)}</body></html>`
203
+ );
204
+ }