@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
package/src/serve-boot.ts CHANGED
@@ -6,11 +6,15 @@
6
6
  // module graph holds neither.
7
7
 
8
8
  import type { DrainConfig, LifecycleOptions, Role } from '@ultimat3/core';
9
- import { configureLifecycle, logger } from '@ultimat3/core';
9
+ import { configureLifecycle, logger, UltimateError } from '@ultimat3/core';
10
+ import { loadAppConfig } from './app-config-load';
11
+ import { armFailureCapture } from './failure-capture-boot';
10
12
  import type { MetricsEndpoint } from './metrics-endpoint';
11
13
  import { startOtlpExport } from './otlp-export';
14
+ import type { Finding } from './output';
12
15
  import { loadAppForRole, roleLoadFor } from './role-load';
13
16
  import { startRoles } from './role-start';
17
+ import { armRowSecurity, rowSecurityDeclared } from './row-security-boot';
14
18
  import { resolveServices } from './runtime-bindings';
15
19
  import { busLabel } from './runtime-bus';
16
20
  import { warnIfIdempotencyProcessScoped } from './runtime-idempotency-scope';
@@ -96,6 +100,29 @@ export async function bootServing(boot: {
96
100
  return bootRoles({ ...boot, runtime });
97
101
  }
98
102
 
103
+ /**
104
+ * A container role whose app did not load whole: `X_APP_MODULE_LOAD_FAILED`. Beside its one thrower
105
+ * (`errors.ts` is at its ceiling). Every environment, not only production: this boot has no watcher
106
+ * to load the module again after a save — that is `x dev`'s, which keeps its own log-and-reload —
107
+ * so a half-loaded app here stays half-loaded, answering `/readyz` without a route or a policy.
108
+ */
109
+ export class AppModuleLoadFailedError extends UltimateError {
110
+ constructor(input: { role: Role; findings: readonly Finding[] }) {
111
+ const modules = input.findings.map((finding) => finding.at ?? '(unknown module)');
112
+ super({
113
+ code: 'X_APP_MODULE_LOAD_FAILED',
114
+ cause: `the ${input.role} role refused to start: ${String(modules.length)} app module(s) failed to load, and serving without them drops their routes, actions, policies and jobs — ${input.findings.map((finding, index) => `${modules[index] ?? ''} (${finding.code})`).join(', ')}`,
115
+ fix: 'x manifest --json # names each module that does not load and why; fix it, then restart the role',
116
+ meta: { role: input.role, modules },
117
+ });
118
+ }
119
+ }
120
+
121
+ /** Throws when any app module failed to load; each was already logged with its own cause. */
122
+ export function refuseHalfLoadedApp(role: Role, findings: readonly Finding[]): void {
123
+ if (findings.length > 0) throw new AppModuleLoadFailedError({ role, findings });
124
+ }
125
+
99
126
  /** The half of `serveApp` whose every acquisition is registered for rollback. */
100
127
  async function bootRoles(boot: {
101
128
  readonly options: ServeOptions;
@@ -106,6 +133,15 @@ async function bootRoles(boot: {
106
133
  readonly metrics: MetricsEndpoint;
107
134
  }): Promise<ServedApp> {
108
135
  const { options, role, runtime, acquired, metrics } = boot;
136
+ // Before the app's modules, because importing one may register a `close` hook, and that phase
137
+ // runs last-registered first: the flush has to be the hook registered earliest, or a teardown's
138
+ // spans are recorded into an exporter already shut down. `OTEL_EXPORTER_OTLP_ENDPOINT` is in the
139
+ // shipped chart and nothing read it, so every deployment that configured a collector got an empty
140
+ // dashboard. `x dev` keeps its own recorder — the `/_x` timeline is a different sink with a
141
+ // different lifetime — so this is the production boot's alone (axiom 6).
142
+ // At the FRONT of the unwind list, so the stop and a failed boot's rollback alike release it
143
+ // last: the drain's own spans and the final counter snapshot still have somewhere to go.
144
+ acquired.unshift(startOtlpExport(options.env));
109
145
  // The image's compiled stylesheets, before the first page is imported — importing one IS
110
146
  // compiling its sheet — and the count of what this boot compiles anyway.
111
147
  adoptPrebuiltStyles(options.root);
@@ -113,23 +149,26 @@ async function bootRoles(boot: {
113
149
  // Importing the app's modules IS the registration: every route, action and job below is
114
150
  // whatever this call put in the registries — for THIS role (`role-load.ts`).
115
151
  const loaded = await loadAppForRole(options.root, role);
116
- // A module that would not import registered nothing, and a process that only logs the roles it
117
- // started gives no sign of it: each one is a line an operator can act on.
152
+ // A module that would not import registered nothing — its routes, actions, policies and jobs
153
+ // are simply absent — so each is logged with its own cause, and then the role refuses to serve.
118
154
  for (const finding of loaded.findings) {
119
155
  logger.error('ultimate app module failed to load', { role, ...finding });
120
156
  }
157
+ refuseHalfLoadedApp(role, loaded.findings);
158
+ // After the app's modules and before any role serves: a role Postgres exempts from row security is
159
+ // refused here (`X_DB_ROW_SECURITY_BYPASSED`), never discovered on another tenant's first read.
160
+ // PGlite's one session is a superuser: there the boot serves as `x_app` instead of refusing.
161
+ const embedded = runtime.services.db.mode === 'embedded';
162
+ const appConfig = await loadAppConfig(options.root);
163
+ acquired.push(await armRowSecurity(rowSecurityDeclared(appConfig), { embedded }));
164
+ // `failureCapture.enabled`: a 5xx keeps a replayable bundle for `x repro` (off by default).
165
+ acquired.push(armFailureCapture(appConfig));
121
166
  // After the app's modules: an app swaps the store at import time, after `startServices`.
122
167
  warnIfIdempotencyProcessScoped();
123
168
  const buildId = await buildIdFor(options, role);
124
169
  // Before the first socket opens: everything above this line fails loudly into the container's
125
170
  // own logs, everything below it is a served request, a claimed job or a routed frame.
126
171
  configureReporting(options.env, buildId);
127
- // Beside error reporting, and for the same reason it is here: `OTEL_EXPORTER_OTLP_ENDPOINT` is
128
- // in the shipped chart and nothing read it, so every deployment that configured a collector got
129
- // an empty dashboard. `x dev` keeps its own recorder — the `/_x` timeline is a different sink
130
- // with a different lifetime — so this is the production boot's alone (axiom 6).
131
- const stopOtlp = startOtlpExport(options.env);
132
- acquired.push(stopOtlp);
133
172
  // Only the web role serves pages, so only it builds islands and assembles a route table: a
134
173
  // worker, scheduler, sync or replicator pod compiled every island of the app on every boot for
135
174
  // nothing it would ever serve (plan 101, slice 12 e). The MODULE is the web role's as well —
@@ -196,18 +235,12 @@ async function bootRoles(boot: {
196
235
  running,
197
236
  runtime,
198
237
  async stop() {
199
- // Every step runs and the first failure is rethrown (`releaseOrThrow`): a refused role stop
200
- // used to leave the services, the PGlite lock and the exporter behind it. Listed in boot
201
- // order, released newest first — the roles; then the web surface, after the server drained
202
- // (a bust landing mid-drain still marks the pages it is serving); then the services; LAST
203
- // the exporter, so the drain's own spans and the final counter snapshot still have
204
- // somewhere to go.
205
- await releaseOrThrow([
206
- stopOtlp,
207
- () => runtime.stop(),
208
- () => web?.release(),
209
- () => running.stop(),
210
- ]);
238
+ // The boot's own unwind list, whole (as `x dev`'s is): every step runs and the first failure
239
+ // is rethrown (`releaseOrThrow`), newest first — the roles; the web surface, after the server
240
+ // drained; the failure-capture store, settled while the pool is still open; the row-security
241
+ // client; the services; the exporter last. A hand-listed copy dropped the two in the middle,
242
+ // so a bundle in flight at drain was lost and the next boot in the process inherited them.
243
+ await releaseOrThrow([...acquired]);
211
244
  },
212
245
  };
213
246
  }
@@ -13,6 +13,7 @@ export async function loadDrainConfig(root: string): Promise<Partial<DrainConfig
13
13
  readinessGraceMs: drain.readinessGraceMs,
14
14
  deadlineMs: drain.deadlineMs,
15
15
  workerDeadlineMs: drain.workerDeadlineMs,
16
+ workerRetireMs: drain.workerRetireMs,
16
17
  };
17
18
  }
18
19
 
package/src/serve-env.ts CHANGED
@@ -1,8 +1,16 @@
1
1
  // What a container's environment decides about a boot: which role, which port, which interface,
2
2
  // and where errors are reported. Split from `serve.ts`, which re-exports every name here.
3
3
 
4
- import type { Role } from '@ultimat3/core';
5
- import { configureErrorReporting, isRole, ROLES, sentryErrorReporter } from '@ultimat3/core';
4
+ import type { ErrorReporter, Role } from '@ultimat3/core';
5
+ import {
6
+ chainErrorReporters,
7
+ configureErrorReporting,
8
+ errorReportersOf,
9
+ installedErrorReporter,
10
+ isRole,
11
+ ROLES,
12
+ sentryErrorReporter,
13
+ } from '@ultimat3/core';
6
14
  import { PortInvalidError, RoleUnknownError } from './errors';
7
15
  import { DEFAULT_METRICS_PORT } from './metrics-endpoint';
8
16
  import type { WebBinding } from './role-start';
@@ -103,17 +111,27 @@ export const metricsPortFor = (env: Env, port: number, override?: number): numbe
103
111
  */
104
112
  export const ERROR_DSN_KEY = 'SENTRY_DSN';
105
113
 
114
+ /** The transport the LAST `configureReporting` installed, so a second boot swaps it, never adds. */
115
+ let vendor: ErrorReporter | undefined;
116
+
106
117
  /**
107
- * Switch reporting on for this process. Unset DSN leaves core's no-op reporter in place, so a
108
- * laptop and a CI run pay nothing and page nobody — and the release every event carries is the
109
- * build id this boot already computed, never a second identity for the same deploy.
118
+ * Switch reporting on for this process. Unset DSN leaves whatever the app installed (or core's
119
+ * no-op) in place, so a laptop and a CI run pay nothing and page nobody — and the release every
120
+ * event carries is the build id this boot already computed, never a second identity for the deploy.
121
+ *
122
+ * The transport is CHAINED beside the app's reporter, never over it: the app's modules import —
123
+ * and install an `errorGroupsSink`, say — before this line runs, and replacing it here would
124
+ * leave `/admin/errors` empty in exactly the deploys that set `SENTRY_DSN`.
110
125
  */
111
126
  export function configureReporting(env: Env, buildId: string): void {
112
127
  const dsn = env[ERROR_DSN_KEY]?.trim();
128
+ // A malformed DSN throws here, at boot, rather than at the first outage: a monitor that was
129
+ // never connected looks exactly like an app that never failed.
130
+ const next = dsn === undefined || dsn.length === 0 ? undefined : sentryErrorReporter({ dsn });
131
+ const app = errorReportersOf(installedErrorReporter()).filter((member) => member !== vendor);
132
+ vendor = next;
113
133
  configureErrorReporting({
114
134
  release: buildId,
115
- // A malformed DSN throws here, at boot, rather than at the first outage: a monitor that was
116
- // never connected looks exactly like an app that never failed.
117
- ...(dsn === undefined || dsn.length === 0 ? {} : { reporter: sentryErrorReporter({ dsn }) }),
135
+ reporter: chainErrorReporters(...app, ...(next === undefined ? [] : [next])),
118
136
  });
119
137
  }
@@ -62,12 +62,18 @@ export function armWorkerRetire(options: WorkerRetireOptions = {}): WorkerRetire
62
62
  const worker = await adopted;
63
63
  try {
64
64
  if (worker !== null) {
65
- const before = await worker.stats();
66
- logger.info('jobs.worker.retiring', {
67
- signal: RETIRE_SIGNAL,
68
- workerId: before.workerId,
69
- inFlight: before.inFlight,
70
- });
65
+ // The log line alone reads stats: a db blip here must not skip the stop, or the drain
66
+ // below is a plain SIGTERM drain and cuts at the budget a job meant to outlast.
67
+ try {
68
+ const before = await worker.stats();
69
+ logger.info('jobs.worker.retiring', {
70
+ signal: RETIRE_SIGNAL,
71
+ workerId: before.workerId,
72
+ inFlight: before.inFlight,
73
+ });
74
+ } catch (error) {
75
+ logger.warn('jobs.worker.retire-stats-failed', { error: renderThrowable(error) });
76
+ }
71
77
  await worker.stop(RETIRE_SIGNAL);
72
78
  const after = await worker.stats();
73
79
  logger.info('jobs.worker.retired', {
package/src/serve-web.ts CHANGED
@@ -3,14 +3,17 @@
3
3
  // `await import()` for `ROLE=web` alone, and `serve-graph.test.ts` pins what every other role is
4
4
  // spared: render's server half, the PWA, SEO, MCP and island subsystems.
5
5
 
6
+ import { resolveEnvironment } from '@ultimat3/core';
6
7
  import type { Route } from '@ultimat3/http';
7
8
  import { appLocaleSet } from '@ultimat3/i18n/app-catalogs';
8
-
9
9
  import { installedVapid } from '@ultimat3/pwa';
10
10
  import { describePages } from '@ultimat3/render';
11
11
  import { apiMountRoutes, apiRoutes, pagePostRoutes } from './api-routes';
12
12
  import { loadSignInPath } from './app-auth';
13
+ import { loadAppConfig } from './app-config-load';
13
14
  import { mountAppMcp } from './app-mcp';
15
+ import { clientErrorsSurface } from './client-errors-routes';
16
+ import { clientScriptHeads, clientVitalsSurface } from './client-vitals-route';
14
17
  import { errorPageStyleBodies } from './error-page-csp';
15
18
  import { islandRoutes } from './island-routes';
16
19
  import type { LoadedIslands } from './island-store';
@@ -18,7 +21,10 @@ import { loadOrBuildIslands } from './island-store';
18
21
  import { loadNavigation, pageNavigation } from './page-navigation';
19
22
  import { loadSpeculation, otherLocaleSegments, pageSpeculation } from './page-speculation';
20
23
  import { pageSync } from './page-sync';
24
+ import { plainRoutes } from './plain-routes';
21
25
  import { loadPwaArtifacts } from './pwa-artifacts';
26
+ import { retainBuildAssets } from './retained-assets-boot';
27
+ import { withRetainedAssets } from './retained-assets-read';
22
28
  import { adminMountRoutes } from './runtime-admin';
23
29
  import { assetRoutes } from './runtime-assets';
24
30
  import { attachedIsr } from './runtime-isr';
@@ -34,6 +40,7 @@ import { serviceWorkerArtifacts } from './sw-artifacts';
34
40
  import { serviceWorkerRoutes } from './sw-routes';
35
41
  import { loadThemeMode, themeBoot } from './theme-boot';
36
42
  import { loadThemeBrand } from './theme-brand';
43
+ import { zoneBoot } from './zone-boot';
37
44
 
38
45
  /** Everything `startRoles` is handed that only a process serving documents has. */
39
46
  export interface WebSurface {
@@ -108,6 +115,29 @@ export async function webSurface(
108
115
  const mcpMount = await mountAppMcp(options.root);
109
116
  // The app's own disks when it declared any (`defineStorage` in an app module), else this boot's.
110
117
  const served = servedStorage(runtime.storage);
118
+ // `assets.retain`: this build's hashed files onto the shared disk, BEFORE the role binds — a pod
119
+ // takes no traffic until `/readyz`, so no document of this build reaches an old pod first.
120
+ const config = await loadAppConfig(options.root);
121
+ // `time.detect`: the zone script on `app/` documents, admitted below by its own hash.
122
+ const zone = zoneBoot(config?.time);
123
+ const scripts = [
124
+ ...sync.scripts,
125
+ ...(navigation.script === undefined ? [] : [navigation.script]),
126
+ ];
127
+ const retained = await retainBuildAssets({
128
+ root: options.root,
129
+ config,
130
+ storage: served,
131
+ buildId,
132
+ environment: resolveEnvironment({ env: options.env, fallback: 'production' }),
133
+ islands,
134
+ styles: styleBundle(),
135
+ scripts,
136
+ });
137
+ // The same call `x dev` makes: `clientErrors.enabled` or nothing mounted and nothing named.
138
+ const clientErrors = clientErrorsSurface(config?.clientErrors, buildId);
139
+ // And real-user timings, on their own switch: the call `x dev` makes, so both name one script.
140
+ const vitals = clientVitalsSurface(config?.clientErrors?.vitals);
111
141
  // Attached, so a tag bust marks this process's ISR pages stale. The store is the deployment's
112
142
  // when it supplied one: the default is per process, so twelve replicas hold twelve of them and a
113
143
  // purge regenerates one twelfth of the fleet while the other eleven keep the invalidated page.
@@ -122,7 +152,7 @@ export async function webSurface(
122
152
  ...apiMountRoutes(options.runtime?.rateLimitStore ?? runtime.rateLimitStore),
123
153
  ...mcpMount.routes,
124
154
  // The app's own plain routes (`apps/<app>/runtime.ts` `routes`), before any page can shadow one.
125
- ...(options.runtime?.routes ?? []),
155
+ ...plainRoutes(options.runtime?.routes),
126
156
  ...(serviceWorker === undefined ? [] : serviceWorkerRoutes(serviceWorker)),
127
157
  ...assetRoutes({
128
158
  root: options.root,
@@ -131,6 +161,8 @@ export async function webSurface(
131
161
  ...(pwa === undefined ? {} : { pwa }),
132
162
  }),
133
163
  ...storageRoutes({ storage: served }),
164
+ ...(clientErrors?.routes ?? []),
165
+ ...(vitals?.routes ?? []),
134
166
  ...islandRoutes(() => islands),
135
167
  // The surface stylesheets the documents link. Built from the registry the `loadApp` above
136
168
  // filled, so this process serves exactly the CSS it renders against.
@@ -169,12 +201,18 @@ export async function webSurface(
169
201
  ...(pwa === undefined
170
202
  ? {}
171
203
  : { pwaHead: (locale: string) => pwa.headFor(locale) + (serviceWorker?.head ?? '') }),
204
+ ...clientScriptHeads(clientErrors, vitals, zone),
172
205
  isr: isr.isr,
173
206
  }),
174
207
  ];
175
208
  return {
176
- routes,
177
- inlineScripts: [theme.cspSource, ...(speculation === undefined ? [] : [speculation.cspSource])],
209
+ // A miss on any asset route asks the retained builds (`assets.retain`); none declared, unchanged.
210
+ routes: withRetainedAssets(routes, retained?.reader),
211
+ inlineScripts: [
212
+ theme.cspSource,
213
+ ...(speculation === undefined ? [] : [speculation.cspSource]),
214
+ ...(zone === undefined ? [] : [zone.cspSource]),
215
+ ],
178
216
  // The enforced policy this process sends must admit the app's own error pages' `<style>` and
179
217
  // the brand's; `x dev` is report-only, so only here was a missing hash an unstyled page.
180
218
  inlineStyles: [
package/src/serve.ts CHANGED
@@ -50,20 +50,22 @@ export type { MigratedApp, ServedApp, ServeOptions, StartedApp } from './serve-t
50
50
  export async function runMigrations(options: ServeOptions): Promise<MigratedApp> {
51
51
  // `'apply'`, never the stamp-first default: this IS the step that applies, and an operator
52
52
  // re-running it is how a dropped framework table comes back.
53
- const queue = await startQueue(
54
- resolveServices(options.root, options.env),
55
- options.runtime,
56
- options.env,
57
- 'apply',
58
- );
53
+ const services = resolveServices(options.root, options.env);
54
+ const queue = await startQueue(services, options.runtime, options.env, 'apply');
59
55
  try {
60
56
  const migrations = await readMigrations(options.root);
57
+ // PGlite with `database.rowSecurity`: the app's migrations run as `x_migrator` for their scope.
58
+ // Behind `await import()` and only embedded: a container's migrate graph never loads it.
59
+ const release =
60
+ services.db.mode === 'embedded'
61
+ ? await (await import('./row-security-boot')).migrateAsMigrator(options.root)
62
+ : undefined;
61
63
  const report = await migrate({
62
64
  migrations,
63
65
  ...(options.env['APP_VERSION'] === undefined
64
66
  ? {}
65
67
  : { appVersion: options.env['APP_VERSION'] }),
66
- });
68
+ }).finally(() => release?.());
67
69
  logger.info('ultimate migrate applied', {
68
70
  applied: report.applied.length,
69
71
  available: migrations.length,
@@ -35,7 +35,13 @@ function assetResponse(asset: SiteAsset, range: string | null): Response {
35
35
  });
36
36
  }
37
37
  if (wanted === undefined) {
38
- return applyCacheHeaders(new Response(file, { headers }), SITE_ASSET_CACHE);
38
+ return applyCacheHeaders(
39
+ new Response(file, {
40
+ // Declared, or `compress` may not read it: a `Bun.file` body states no length of its own.
41
+ headers: { ...headers, 'content-length': String(asset.bytes) },
42
+ }),
43
+ SITE_ASSET_CACHE,
44
+ );
39
45
  }
40
46
  return applyCacheHeaders(
41
47
  new Response(file.slice(wanted.start, wanted.end + 1), {
@@ -26,6 +26,8 @@ export interface SiteSettings {
26
26
  * `SiteSettings` literal written before it existed still compiles.
27
27
  */
28
28
  readonly favicon?: SiteFavicon | undefined;
29
+ /** `seo.llms` — `/llms.txt` on (the default) or off. Optional for `robotsGroups`' reason. */
30
+ readonly llms?: boolean | undefined;
29
31
  }
30
32
 
31
33
  export const NO_SITE_SETTINGS: SiteSettings = {
@@ -34,6 +36,7 @@ export const NO_SITE_SETTINGS: SiteSettings = {
34
36
  robotsGroups: [],
35
37
  sitemap: { extra: [], lastmod: 'none', sections: [], pageSize: 50_000 },
36
38
  favicon: undefined,
39
+ llms: true,
37
40
  };
38
41
 
39
42
  const trimSlash = (origin: string): string => origin.replace(/\/+$/, '');
@@ -48,6 +51,7 @@ export async function loadSiteSettings(root: string): Promise<SiteSettings> {
48
51
  robotsGroups: [...(config.seo.robots.groups ?? [])],
49
52
  sitemap: config.seo.sitemap,
50
53
  favicon,
54
+ llms: config.seo.llms !== false,
51
55
  };
52
56
  }
53
57
 
@@ -0,0 +1,106 @@
1
+ // `/llms.txt`'s per-page reads: every page of a public `site/` route whose `meta` needs its `load`
2
+ // (or its params), titled by that `meta`. Collected by the static export INSIDE its API scope, so
3
+ // `siteLlms` never runs a page's `load` after the build stopped answering the app's own queries.
4
+
5
+ import type { Environment } from '@ultimat3/core';
6
+ import { ctxOf, DEFAULT_ENVIRONMENT, runWithContext, tryResolveEnvironment } from '@ultimat3/core';
7
+ import type { PrerenderEntry, RouteEntry } from '@ultimat3/render';
8
+ import { metaContextFor, routeDataFor, routeEntries } from '@ultimat3/render';
9
+ import { enumeratePrerenderEntries, fillPath } from '@ultimat3/render/server';
10
+ import type { RouteRecord } from '@ultimat3/seo';
11
+ import { isIndexable } from '@ultimat3/seo';
12
+ import type { RouteEntries } from './prerender-entries';
13
+ import { readSiteMeta } from './seo-meta';
14
+
15
+ /** Each route's page records, by DECLARED path (`/posts/:slug`) — what `siteLlms` reads first. */
16
+ export type LlmsPages = ReadonlyMap<string, readonly RouteRecord[]>;
17
+
18
+ export type Enumerate = (entry: RouteEntry) => Promise<readonly PrerenderEntry[]>;
19
+
20
+ export interface LlmsPageOptions {
21
+ readonly baseUrl: string;
22
+ /** The app's default locale: every `meta` runs in it. */
23
+ readonly defaultLocale: string;
24
+ /** The build's own enumeration (`PrerenderReport.entries`), so `prerender()` is asked once. */
25
+ readonly enumerate?: Enumerate | undefined;
26
+ }
27
+
28
+ /** A `site/` page anyone may fetch — `site-seo.ts`' own rule. */
29
+ const isPublicSite = (entry: RouteEntry): boolean =>
30
+ entry.surface === 'site' && entry.config.policy === undefined;
31
+
32
+ /** `llms.txt`'s routes: those whose `meta` resolves without a request, and those that need `load`. */
33
+ export interface LlmsRoutes {
34
+ readonly resolved: readonly RouteRecord[];
35
+ readonly loaded: readonly RouteEntry[];
36
+ }
37
+
38
+ /**
39
+ * The public `site/` routes, split once: `resolved` answered by `readSiteMeta` (no `load`, no
40
+ * params), `loaded` the rest, which `pageRecords` reads page by page.
41
+ */
42
+ export async function llmsRoutes(defaultLocale: string): Promise<LlmsRoutes> {
43
+ const entries = routeEntries().filter(isPublicSite);
44
+ const publicPaths = new Set(entries.map((entry) => entry.path));
45
+ const resolved = (await readSiteMeta(defaultLocale)).records.filter((record) =>
46
+ publicPaths.has(record.path),
47
+ );
48
+ const resolvedPaths = new Set(resolved.map((record) => record.path));
49
+ return { resolved, loaded: entries.filter((entry) => !resolvedPaths.has(entry.path)) };
50
+ }
51
+
52
+ /**
53
+ * One record per page `prerender()` enumerates, with the `meta` its own `load` feeds, run in the
54
+ * default locale with no actor: the prerender's context.
55
+ */
56
+ export async function pageRecords(
57
+ entry: RouteEntry,
58
+ options: LlmsPageOptions,
59
+ ): Promise<readonly RouteRecord[]> {
60
+ const records: RouteRecord[] = [];
61
+ for (const page of await (options.enumerate ?? enumeratePrerenderEntries)(entry)) {
62
+ const path = fillPath(entry.pattern.source, page.params);
63
+ const ctx = { url: `${options.baseUrl.replace(/\/+$/, '')}${path}`, params: page.params };
64
+ const meta = await runWithContext(
65
+ ctxOf({ role: 'web', locale: options.defaultLocale }),
66
+ async () => entry.config.meta(metaContextFor(ctx, await routeDataFor(entry.config, ctx))),
67
+ );
68
+ records.push({ path, file: entry.file, surface: 'site', render: entry.config.render, meta });
69
+ }
70
+ return records;
71
+ }
72
+
73
+ export interface CollectLlmsPagesOptions extends Omit<LlmsPageOptions, 'enumerate'> {
74
+ /** The build's own `prerender()` answers (`enumerateRoutes`): a route listed is never asked again. */
75
+ readonly entries?: RouteEntries | undefined;
76
+ readonly environment?: Environment | undefined;
77
+ /** `seo.llms`; `false` collects nothing. */
78
+ readonly enabled?: boolean | undefined;
79
+ /** The build's API scope: every `load` runs inside it, where the app's own queries answer. */
80
+ readonly scope: <T>(fn: () => Promise<T>) => Promise<T>;
81
+ /** Before the first `load`: the build starts its database here, and only when one is needed. */
82
+ readonly before?: (() => Promise<void>) | undefined;
83
+ }
84
+
85
+ /**
86
+ * Every page record `siteLlms` would read through a `load`, read now, inside `scope`. Nothing
87
+ * outside production or under `seo.llms: false` — `siteLlms` answers empty there, so a staging
88
+ * build reads no row for a document it throws away. A throw is the build's failure: a `load` that
89
+ * cannot answer is the app's bug, never a page silently left out.
90
+ */
91
+ export async function collectLlmsPages(options: CollectLlmsPagesOptions): Promise<LlmsPages> {
92
+ const environment = options.environment ?? tryResolveEnvironment() ?? DEFAULT_ENVIRONMENT;
93
+ const pages = new Map<string, readonly RouteRecord[]>();
94
+ if (options.enabled === false || !isIndexable(environment)) return pages;
95
+ const routes = (await llmsRoutes(options.defaultLocale)).loaded;
96
+ if (routes.length === 0) return pages;
97
+ await options.before?.();
98
+ const enumerate: Enumerate = (route) => {
99
+ const listed = options.entries?.get(route.path);
100
+ return listed === undefined ? enumeratePrerenderEntries(route) : Promise.resolve(listed);
101
+ };
102
+ for (const entry of routes) {
103
+ pages.set(entry.path, await options.scope(() => pageRecords(entry, { ...options, enumerate })));
104
+ }
105
+ return pages;
106
+ }
@@ -0,0 +1,64 @@
1
+ // `/llms.txt`'s pages, off the route table: every public `site/` page in the default locale by the
2
+ // title and description its own `meta` gives it. A dynamic route (or one with a `load`) is expanded
3
+ // through `prerender()` and its `load`, page by page, exactly as the static export renders it — or
4
+ // read off what the export already collected inside its API scope (`site-llms-pages.ts`).
5
+
6
+ import type { Environment } from '@ultimat3/core';
7
+ import { DEFAULT_ENVIRONMENT, tryResolveEnvironment } from '@ultimat3/core';
8
+ import { unlocalizedPath } from '@ultimat3/i18n';
9
+ import type { RouteRecord } from '@ultimat3/seo';
10
+ import { buildLlmsTxt, disallowMatcher, isDynamic, isIndexable } from '@ultimat3/seo';
11
+ import type { Enumerate, LlmsPages } from './site-llms-pages';
12
+ import { llmsRoutes, pageRecords } from './site-llms-pages';
13
+
14
+ export interface SiteLlmsOptions {
15
+ readonly baseUrl: string;
16
+ readonly environment?: Environment | undefined;
17
+ /** `seo.llms`; `false` answers an empty document. */
18
+ readonly enabled?: boolean | undefined;
19
+ readonly disallow?: readonly string[] | undefined;
20
+ /** The static export's emitted pages per route, as the sitemap reads them (`SiteSeoOptions`). */
21
+ readonly pagesFor?: ((routePath: string) => readonly string[]) | undefined;
22
+ /** The app's default locale: every `meta` runs in it, and its paths are unprefixed. */
23
+ readonly defaultLocale: string;
24
+ /** `siteSeo`'s one enumeration per route, so the sitemap and this file never call `prerender()` twice. */
25
+ readonly enumerate?: Enumerate | undefined;
26
+ /**
27
+ * What the static export read inside its API scope (`collectLlmsPages`), by declared route path.
28
+ * A route listed here never runs its `load` again: by the time `llms.txt` is written the build's
29
+ * scope is gone, and a `load` reading the app's own query failed the export
30
+ * (`X_CLIENT_TRANSPORT_FAILED`).
31
+ */
32
+ readonly pages?: LlmsPages | undefined;
33
+ }
34
+
35
+ /**
36
+ * Empty outside production and under `seo.llms: false` BEFORE any `prerender()` or `load` runs: a
37
+ * staging web role must not read every post to answer a document it then throws away.
38
+ */
39
+ export async function siteLlms(options: SiteLlmsOptions): Promise<string> {
40
+ const environment = options.environment ?? tryResolveEnvironment() ?? DEFAULT_ENVIRONMENT;
41
+ if (options.enabled === false || !isIndexable(environment)) return '';
42
+ const disallowed = disallowMatcher(options.disallow ?? []);
43
+ const { resolved, loaded } = await llmsRoutes(options.defaultLocale);
44
+ const expanded: RouteRecord[] = [];
45
+ for (const entry of loaded) {
46
+ const records = options.pages?.get(entry.path) ?? (await pageRecords(entry, options));
47
+ expanded.push(...emittedOnly(entry.path, records, options));
48
+ }
49
+ return buildLlmsTxt(
50
+ [...resolved, ...expanded].filter((record) => !disallowed(record.path)),
51
+ { baseUrl: options.baseUrl, environment },
52
+ );
53
+ }
54
+
55
+ /** A dynamic route's pages, less those the export did not emit — the sitemap's own rule. */
56
+ function emittedOnly(
57
+ routePath: string,
58
+ records: readonly RouteRecord[],
59
+ options: SiteLlmsOptions,
60
+ ): readonly RouteRecord[] {
61
+ if (options.pagesFor === undefined || !isDynamic(routePath)) return records;
62
+ const emitted = new Set(options.pagesFor(routePath).map((path) => unlocalizedPath(path)));
63
+ return records.filter((record) => emitted.has(record.path));
64
+ }
@@ -1,8 +1,9 @@
1
1
  // Single responsibility: `siteSeo()`'s answer as files in a static export — `/sitemap.xml`, every
2
- // part an index names, and `robots.txt` — so a CDN serves the bytes a running web role answers.
2
+ // part an index names, `robots.txt` and `llms.txt` — so a CDN serves the bytes a running web role answers.
3
3
 
4
4
  // why: Bun exposes no path-join primitive, and every file path is relative to the output root.
5
5
  import { join } from 'node:path';
6
+ import { LLMS_PATH } from '@ultimat3/seo';
6
7
  import { ROBOTS_PATH, type SiteSeo } from './site-seo';
7
8
 
8
9
  /**
@@ -12,5 +13,6 @@ import { ROBOTS_PATH, type SiteSeo } from './site-seo';
12
13
  export async function writeSiteSeo(out: string, seo: SiteSeo): Promise<readonly string[]> {
13
14
  for (const file of seo.sitemaps) await Bun.write(join(out, file.path), file.xml);
14
15
  await Bun.write(join(out, ROBOTS_PATH), seo.robots);
15
- return [...seo.sitemaps.map((file) => file.path), ROBOTS_PATH];
16
+ await Bun.write(join(out, LLMS_PATH), seo.llms);
17
+ return [...seo.sitemaps.map((file) => file.path), ROBOTS_PATH, LLMS_PATH];
16
18
  }