@ultimat3/cli 29.4.0 → 30.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (448) hide show
  1. package/CLAUDE.md +64 -40
  2. package/README.md +61 -11
  3. package/package.json +42 -28
  4. package/src/affected-tests.ts +97 -0
  5. package/src/affected-unowned.ts +43 -0
  6. package/src/affected.ts +25 -6
  7. package/src/app-boundaries.ts +29 -1
  8. package/src/app-evals.ts +12 -4
  9. package/src/app-load-unresolved.ts +62 -0
  10. package/src/app-load.ts +30 -37
  11. package/src/app-mcp.ts +14 -2
  12. package/src/app-module-graph.ts +232 -0
  13. package/src/app-reload-graph.ts +2 -2
  14. package/src/app-root.ts +37 -1
  15. package/src/bin.ts +1 -0
  16. package/src/boundary-findings.ts +6 -0
  17. package/src/budgets-freshness.ts +32 -0
  18. package/src/budgets.ts +20 -1
  19. package/src/build-capture-env.ts +9 -0
  20. package/src/cdp-shot-a11y.ts +2 -39
  21. package/src/ci-tested.ts +243 -0
  22. package/src/client-emit-customer-runtime.ts +139 -0
  23. package/src/client-emit-customer.ts +108 -0
  24. package/src/client-emit-drift.ts +72 -0
  25. package/src/client-emit-header.ts +67 -0
  26. package/src/client-emit-schemas.ts +175 -0
  27. package/src/client-emit-types.ts +121 -0
  28. package/src/client-emit-vendor.ts +146 -0
  29. package/src/client-errors-routes.ts +229 -0
  30. package/src/client-errors-script.ts +74 -0
  31. package/src/client-vitals-route.ts +197 -0
  32. package/src/client-vitals-script.ts +56 -0
  33. package/src/cmd-affected.ts +23 -0
  34. package/src/cmd-build-spec.ts +7 -1
  35. package/src/cmd-build.ts +35 -9
  36. package/src/cmd-ci-spec.ts +42 -6
  37. package/src/cmd-ci.ts +99 -2
  38. package/src/cmd-client-customer.ts +134 -0
  39. package/src/cmd-client-source.ts +81 -0
  40. package/src/cmd-client-spec.ts +48 -0
  41. package/src/cmd-client-write.ts +41 -0
  42. package/src/cmd-client.ts +111 -0
  43. package/src/cmd-db-recount.ts +120 -0
  44. package/src/cmd-db-spec.ts +36 -6
  45. package/src/cmd-db.ts +11 -3
  46. package/src/cmd-deploy-compose.ts +61 -3
  47. package/src/cmd-deploy-drain.ts +10 -2
  48. package/src/cmd-deploy-helm.ts +11 -0
  49. package/src/cmd-deploy-preflight.ts +118 -0
  50. package/src/cmd-deploy-spec.ts +58 -6
  51. package/src/cmd-deploy-verify.ts +380 -0
  52. package/src/cmd-deploy.ts +42 -9
  53. package/src/cmd-docs.ts +34 -4
  54. package/src/cmd-doctor.ts +66 -9
  55. package/src/cmd-drill-spec.ts +25 -0
  56. package/src/cmd-drill.ts +176 -0
  57. package/src/cmd-env-init.ts +102 -0
  58. package/src/cmd-env-spec.ts +10 -3
  59. package/src/cmd-env.ts +15 -10
  60. package/src/cmd-errors-recent.ts +60 -0
  61. package/src/cmd-errors-spec.ts +9 -1
  62. package/src/cmd-errors.ts +4 -0
  63. package/src/cmd-exercise-spec.ts +19 -0
  64. package/src/cmd-exercise.ts +129 -0
  65. package/src/cmd-generate-spec.ts +9 -1
  66. package/src/cmd-generate.ts +32 -27
  67. package/src/cmd-graph-load.ts +97 -0
  68. package/src/cmd-graph-spec.ts +16 -0
  69. package/src/cmd-graph.ts +73 -0
  70. package/src/cmd-i18n.ts +4 -1
  71. package/src/cmd-impact-spec.ts +13 -0
  72. package/src/cmd-impact.ts +64 -0
  73. package/src/cmd-jobs-resolve.ts +76 -0
  74. package/src/cmd-jobs-spec.ts +21 -2
  75. package/src/cmd-jobs.ts +10 -6
  76. package/src/cmd-mail-spec.ts +28 -0
  77. package/src/cmd-mail.ts +70 -0
  78. package/src/cmd-mcp-lock.ts +69 -0
  79. package/src/cmd-mcp-spec.ts +23 -4
  80. package/src/cmd-mcp.ts +17 -3
  81. package/src/cmd-new-spec.ts +8 -1
  82. package/src/cmd-new.ts +61 -9
  83. package/src/cmd-planned.ts +0 -6
  84. package/src/cmd-repro-spec.ts +31 -0
  85. package/src/cmd-repro.ts +174 -0
  86. package/src/cmd-stop-spec.ts +21 -0
  87. package/src/cmd-stop.ts +166 -0
  88. package/src/cmd-test-spec.ts +17 -4
  89. package/src/cmd-test.ts +70 -14
  90. package/src/cmd-upgrade-spec.ts +26 -0
  91. package/src/cmd-upgrade.ts +274 -0
  92. package/src/cmd-verify-spec.ts +3 -3
  93. package/src/db-backfill.ts +6 -0
  94. package/src/db-diagnostics.ts +106 -0
  95. package/src/db-generate.ts +9 -0
  96. package/src/db-reset-template.ts +1 -1
  97. package/src/db-rolling.ts +45 -0
  98. package/src/db-seed-generate.ts +188 -0
  99. package/src/deploy-verify-findings.ts +98 -0
  100. package/src/deploy-verify-flags.ts +109 -0
  101. package/src/deploy-verify-grade.ts +185 -0
  102. package/src/deploy-verify-queues.ts +53 -0
  103. package/src/dev-boot.ts +35 -15
  104. package/src/dev-child-watch.ts +16 -4
  105. package/src/dev-dashboard-guard.ts +8 -1
  106. package/src/dev-dashboard.ts +7 -0
  107. package/src/dev-lock-identity.ts +61 -0
  108. package/src/dev-lock-port-holder.ts +80 -0
  109. package/src/dev-lock-steal.ts +124 -0
  110. package/src/dev-lock.ts +92 -120
  111. package/src/dev-mail-templates.ts +51 -0
  112. package/src/dev-rebuild.ts +26 -2
  113. package/src/dev-route-table.ts +17 -0
  114. package/src/dev-supervisor.ts +5 -2
  115. package/src/dev-watch-tree.ts +11 -10
  116. package/src/dev-watch.ts +1 -1
  117. package/src/dispatch.ts +29 -6
  118. package/src/docs-section.ts +69 -0
  119. package/src/docs-sources.ts +100 -0
  120. package/src/doctor-auth.ts +11 -2
  121. package/src/doctor-chart-alerts.ts +55 -0
  122. package/src/doctor-pooler.ts +107 -0
  123. package/src/doctor-rolling.ts +35 -0
  124. package/src/doctor-row-security.ts +40 -0
  125. package/src/document-options.ts +13 -0
  126. package/src/drill-sync-lane.ts +25 -0
  127. package/src/drill-targets.ts +39 -0
  128. package/src/duplicate-packages.ts +1 -1
  129. package/src/env-production.ts +93 -0
  130. package/src/error-codes-shot.ts +59 -0
  131. package/src/error-codes-tooling.ts +29 -0
  132. package/src/error-codes.ts +100 -54
  133. package/src/error-contract.ts +2 -2
  134. package/src/errors.ts +22 -3
  135. package/src/exercise-database.ts +99 -0
  136. package/src/exercise-findings.ts +159 -0
  137. package/src/exercise-overrides.ts +42 -0
  138. package/src/exercise-registration.ts +58 -0
  139. package/src/exercise-static.ts +88 -0
  140. package/src/exercise-subject.ts +69 -0
  141. package/src/failure-capture-boot.ts +30 -0
  142. package/src/framework-env.ts +7 -1
  143. package/src/framework-schema.ts +72 -5
  144. package/src/generate-catalog-locales.ts +19 -0
  145. package/src/generate-chart-alerts.ts +156 -0
  146. package/src/generate-feature-required.ts +65 -0
  147. package/src/generate-feature.ts +83 -6
  148. package/src/generate-files.ts +24 -0
  149. package/src/generate-format-written.ts +32 -0
  150. package/src/generate-kinds.ts +6 -0
  151. package/src/generate-sign-in-config.ts +100 -0
  152. package/src/generate-sign-in-manifest.ts +28 -0
  153. package/src/generate-sign-in-mode.ts +101 -0
  154. package/src/generate-sign-in.ts +250 -0
  155. package/src/generate-write.ts +44 -7
  156. package/src/hand-rolled-socket.ts +72 -0
  157. package/src/i18n-app-key-shapes.ts +29 -0
  158. package/src/i18n-audit.ts +10 -3
  159. package/src/i18n-registration.ts +18 -3
  160. package/src/index.ts +2 -0
  161. package/src/island-bundle.ts +4 -3
  162. package/src/island-disposer.ts +116 -0
  163. package/src/island-realtime-state.ts +34 -6
  164. package/src/island-realtime.ts +6 -2
  165. package/src/island-routes.ts +3 -2
  166. package/src/isr-query-advice.ts +6 -11
  167. package/src/isr-query-guard.ts +4 -3
  168. package/src/job-budget.ts +95 -0
  169. package/src/jobs-json.ts +11 -0
  170. package/src/lint-findings.ts +104 -0
  171. package/src/local-cli.ts +34 -4
  172. package/src/loopback-host.ts +2 -2
  173. package/src/mail-preview-findings.ts +24 -0
  174. package/src/mcp-errors.ts +82 -2
  175. package/src/mcp-host.ts +11 -1
  176. package/src/mcp-lock-findings.ts +57 -0
  177. package/src/measure-paths.ts +12 -4
  178. package/src/measure-scope.ts +27 -13
  179. package/src/messages-client.ts +23 -0
  180. package/src/messages-db.ts +56 -0
  181. package/src/messages-gate.ts +107 -0
  182. package/src/messages-repro.ts +11 -0
  183. package/src/messages-shot.ts +51 -0
  184. package/src/messages-upgrade.ts +45 -0
  185. package/src/messages.ts +53 -178
  186. package/src/metrics-endpoint.ts +23 -5
  187. package/src/new-shapes-generate.ts +71 -0
  188. package/src/new-shapes.ts +63 -0
  189. package/src/openapi-compile-ir.ts +68 -0
  190. package/src/openapi-compile-schema.ts +284 -0
  191. package/src/openapi-compile.ts +273 -0
  192. package/src/openapi-refuse.ts +45 -0
  193. package/src/otlp-export.ts +2 -1
  194. package/src/output-findings.ts +123 -0
  195. package/src/output.ts +32 -8
  196. package/src/package-boundaries.ts +110 -0
  197. package/src/page-pins.ts +23 -0
  198. package/src/plain-routes.ts +13 -0
  199. package/src/prerender-entries.ts +59 -0
  200. package/src/prerender-heaviest.ts +27 -0
  201. package/src/prerender-preload.ts +6 -0
  202. package/src/prerender-report.ts +12 -0
  203. package/src/prerender-seo-reads.ts +38 -0
  204. package/src/prerender-served.ts +22 -7
  205. package/src/prerender-vitals.ts +42 -0
  206. package/src/prerender.ts +61 -32
  207. package/src/pwa-artifacts.ts +4 -3
  208. package/src/pwa-manifest-locales.ts +1 -1
  209. package/src/registry.ts +18 -0
  210. package/src/repro-bundle.ts +65 -0
  211. package/src/repro-errors.ts +47 -0
  212. package/src/repro-job.ts +72 -0
  213. package/src/repro-literal.ts +41 -0
  214. package/src/repro-request.ts +48 -0
  215. package/src/repro-write.ts +100 -0
  216. package/src/retain-sweep.ts +27 -0
  217. package/src/retained-assets-boot.ts +113 -0
  218. package/src/retained-assets-layout.ts +110 -0
  219. package/src/retained-assets-read.ts +142 -0
  220. package/src/retained-assets-sweep.ts +70 -0
  221. package/src/retained-assets.ts +171 -0
  222. package/src/revalidated-response.ts +8 -5
  223. package/src/role-replicator.ts +7 -1
  224. package/src/role-start-devices.ts +119 -0
  225. package/src/role-start-types.ts +6 -0
  226. package/src/role-start.ts +22 -5
  227. package/src/role-sync.ts +7 -1
  228. package/src/route-islands.ts +42 -0
  229. package/src/route-table.ts +2 -1
  230. package/src/row-security-boot.ts +58 -0
  231. package/src/row-security-wanted.ts +32 -0
  232. package/src/runtime-admin.ts +0 -1
  233. package/src/runtime-assets-site-cache.ts +55 -0
  234. package/src/runtime-assets-site.ts +136 -0
  235. package/src/runtime-assets.ts +99 -55
  236. package/src/runtime-bindings.ts +19 -3
  237. package/src/runtime-cache.ts +23 -14
  238. package/src/runtime-isr-outcome.ts +114 -60
  239. package/src/runtime-purge.ts +14 -2
  240. package/src/runtime-queue.ts +31 -4
  241. package/src/runtime-render-document.ts +204 -0
  242. package/src/runtime-render.ts +48 -198
  243. package/src/runtime-replica-position.ts +62 -0
  244. package/src/runtime-replica.ts +18 -3
  245. package/src/runtime-s3-path-style.ts +17 -0
  246. package/src/runtime-services.ts +19 -6
  247. package/src/runtime-statement-bound.ts +22 -0
  248. package/src/runtime-storage-upload.ts +9 -0
  249. package/src/runtime-storage.ts +41 -81
  250. package/src/schema-diff-views.ts +54 -0
  251. package/src/schema-diff.ts +21 -3
  252. package/src/schema-drift.ts +28 -9
  253. package/src/schema-dump-drift.ts +91 -7
  254. package/src/script-csp.ts +1 -1
  255. package/src/seo-cards.ts +26 -0
  256. package/src/seo-routes.ts +22 -8
  257. package/src/serve-boot.ts +54 -21
  258. package/src/serve-drain.ts +1 -0
  259. package/src/serve-env.ts +26 -8
  260. package/src/serve-retire.ts +12 -6
  261. package/src/serve-web.ts +42 -4
  262. package/src/serve.ts +9 -7
  263. package/src/site-asset-routes.ts +7 -1
  264. package/src/site-config.ts +4 -0
  265. package/src/site-llms-pages.ts +106 -0
  266. package/src/site-llms.ts +64 -0
  267. package/src/site-seo-write.ts +4 -2
  268. package/src/site-seo.ts +55 -6
  269. package/src/source-mtime.ts +50 -0
  270. package/src/static-capture-errors.ts +41 -0
  271. package/src/static-capture.ts +56 -0
  272. package/src/static-cards-serve.ts +39 -0
  273. package/src/static-cards.ts +93 -0
  274. package/src/static-document.ts +14 -13
  275. package/src/static-images.ts +115 -0
  276. package/src/stream-hole-scope.ts +55 -0
  277. package/src/style-bundle.ts +1 -1
  278. package/src/style-routes.ts +3 -2
  279. package/src/sw-precache-plan.ts +2 -2
  280. package/src/sw-routes.ts +2 -2
  281. package/src/templates/action-tests.ts +306 -0
  282. package/src/templates/action.ts +81 -269
  283. package/src/templates/backfill.ts +1 -1
  284. package/src/templates/entity.ts +21 -5
  285. package/src/templates/github/ci.yml.ts +8 -9
  286. package/src/templates/guard-bare-error.ts +2 -2
  287. package/src/templates/guard-error-declared.ts +263 -0
  288. package/src/templates/guard-native-confirm.ts +132 -0
  289. package/src/templates/guard-raw-fetch.ts +151 -0
  290. package/src/templates/guard-raw-interval.ts +121 -0
  291. package/src/templates/guard-repo-raw-sql.ts +71 -2
  292. package/src/templates/job.ts +1 -1
  293. package/src/templates/naming.ts +5 -0
  294. package/src/templates/pinned/docker-compose.yml +6 -0
  295. package/src/templates/pinned-images.ts +8 -0
  296. package/src/templates/policy.ts +85 -2
  297. package/src/templates/query.ts +70 -13
  298. package/src/templates/resource-create.ts +3 -1
  299. package/src/templates/resource-form-island.ts +15 -4
  300. package/src/templates/resource-page-db.ts +83 -0
  301. package/src/templates/resource-page.ts +161 -52
  302. package/src/templates/route.ts +4 -6
  303. package/src/templates/scaffold-app.ts +3 -0
  304. package/src/templates/scaffold-auth.ts +2 -1
  305. package/src/templates/scaffold-bin.ts +5 -52
  306. package/src/templates/scaffold-claude-agents.ts +19 -19
  307. package/src/templates/scaffold-claude-commands.ts +28 -31
  308. package/src/templates/scaffold-claude.ts +26 -23
  309. package/src/templates/scaffold-container-compose.ts +6 -1
  310. package/src/templates/scaffold-container.ts +24 -4
  311. package/src/templates/scaffold-dashboard-bare.ts +3 -3
  312. package/src/templates/scaffold-dashboard-example-tests.ts +49 -9
  313. package/src/templates/scaffold-dashboard-example-view.ts +156 -0
  314. package/src/templates/scaffold-dashboard-example.ts +43 -131
  315. package/src/templates/scaffold-dashboard-shared.ts +3 -7
  316. package/src/templates/scaffold-db-client.ts +20 -6
  317. package/src/templates/scaffold-db-package.ts +5 -14
  318. package/src/templates/scaffold-docs.ts +73 -57
  319. package/src/templates/scaffold-domain-package.ts +18 -14
  320. package/src/templates/scaffold-entries.ts +7 -2
  321. package/src/templates/scaffold-exercise.ts +37 -0
  322. package/src/templates/scaffold-guards.ts +13 -1
  323. package/src/templates/scaffold-helm-alerts.ts +234 -0
  324. package/src/templates/scaffold-helm-templates.ts +7 -5
  325. package/src/templates/scaffold-helm.ts +46 -12
  326. package/src/templates/scaffold-i18n.ts +6 -2
  327. package/src/templates/scaffold-mcp-endpoint.ts +182 -0
  328. package/src/templates/scaffold-mcp-json.ts +31 -0
  329. package/src/templates/scaffold-mcp-package.ts +59 -56
  330. package/src/templates/scaffold-repo.ts +7 -2
  331. package/src/templates/scaffold-shell.ts +45 -79
  332. package/src/templates/scaffold-test-services.ts +13 -5
  333. package/src/templates/shapes/api-keys.ts +170 -0
  334. package/src/templates/shapes/api.ts +101 -0
  335. package/src/templates/shapes/blog-site.ts +340 -0
  336. package/src/templates/shapes/blog-slice.ts +265 -0
  337. package/src/templates/shapes/blog.ts +114 -0
  338. package/src/templates/shapes/dashboard-detail.ts +242 -0
  339. package/src/templates/shapes/dashboard.ts +53 -0
  340. package/src/templates/shapes/index.ts +17 -0
  341. package/src/templates/shapes/overlay.ts +99 -0
  342. package/src/templates/shapes/shared-edits.ts +46 -0
  343. package/src/templates/sign-in-action-tests.ts +237 -0
  344. package/src/templates/sign-in-authenticator.ts +112 -0
  345. package/src/templates/sign-in-oidc.ts +189 -0
  346. package/src/templates/sign-in-pages.ts +148 -0
  347. package/src/templates/sign-in-password.ts +216 -0
  348. package/src/templates/sign-in-shared.ts +268 -0
  349. package/src/templates/slice-foundation.ts +70 -15
  350. package/src/test-batches.ts +1 -1
  351. package/src/test-database.ts +1 -1
  352. package/src/test-shards.ts +7 -1
  353. package/src/test-workers.ts +18 -12
  354. package/src/tool-gate-names.ts +34 -0
  355. package/src/upgrade-agents-link.ts +49 -0
  356. package/src/upgrade-bun-floor.ts +225 -0
  357. package/src/upgrade-gate.ts +228 -0
  358. package/src/upgrade-pins.ts +63 -0
  359. package/src/upgrade-plan.ts +214 -0
  360. package/src/upgrade-registry.ts +53 -0
  361. package/src/upgrade-report.ts +139 -0
  362. package/src/upgrade-root.ts +74 -0
  363. package/src/verify-budgets-build.ts +118 -0
  364. package/src/verify-checks.ts +67 -24
  365. package/src/verify-coverage-run.ts +1 -1
  366. package/src/verify-e2e-lane.ts +41 -0
  367. package/src/verify-exercise.ts +64 -0
  368. package/src/verify-floor.ts +41 -0
  369. package/src/verify-merge.ts +19 -4
  370. package/src/verify-role-load.ts +2 -0
  371. package/src/verify-run.ts +28 -8
  372. package/src/verify-shard.ts +12 -2
  373. package/src/verify-skip-ceiling.ts +87 -0
  374. package/src/verify-step.ts +5 -0
  375. package/src/verify-tests.ts +25 -5
  376. package/src/view-drift.ts +55 -0
  377. package/src/worker-bundle.ts +3 -2
  378. package/src/workspace-graph.ts +38 -19
  379. package/src/write-generated.ts +43 -0
  380. package/src/zone-boot.ts +20 -0
  381. package/wiki/Actions.md +342 -0
  382. package/wiki/Admin-Dashboard.md +432 -0
  383. package/wiki/Agents.md +958 -0
  384. package/wiki/Auth.md +195 -0
  385. package/wiki/Bare-VM.md +111 -0
  386. package/wiki/Batching-And-Preloading.md +136 -0
  387. package/wiki/Building-Your-Own-Base.md +203 -0
  388. package/wiki/CI-Parallel-Gate.md +220 -0
  389. package/wiki/CLI-Reference.md +1894 -0
  390. package/wiki/Caching-And-Invalidation.md +255 -0
  391. package/wiki/Client-Data.md +194 -0
  392. package/wiki/Client-Navigation.md +513 -0
  393. package/wiki/Compared.md +93 -0
  394. package/wiki/Configuration.md +868 -0
  395. package/wiki/Contributing.md +12 -0
  396. package/wiki/Deploy-Recipes.md +122 -0
  397. package/wiki/Deployment.md +508 -0
  398. package/wiki/Entities-And-Migrations.md +1116 -0
  399. package/wiki/FAQ.md +175 -0
  400. package/wiki/Feature-Flags.md +65 -0
  401. package/wiki/Getting-Started.md +221 -0
  402. package/wiki/Home.md +160 -0
  403. package/wiki/I18n.md +239 -0
  404. package/wiki/Installation.md +206 -0
  405. package/wiki/Interface-Rules.md +339 -0
  406. package/wiki/Jobs-And-Workflows.md +783 -0
  407. package/wiki/Known-Gaps.md +185 -0
  408. package/wiki/MCP-And-AI.md +753 -0
  409. package/wiki/Mail.md +138 -0
  410. package/wiki/Migrating-An-Existing-App.md +594 -0
  411. package/wiki/Migrating-From-Django.md +54 -0
  412. package/wiki/Migrating-From-Laravel.md +53 -0
  413. package/wiki/Migrating-From-Node-Drizzle.md +53 -0
  414. package/wiki/Migrating-From-Rails.md +53 -0
  415. package/wiki/Migrations-And-Backfills.md +552 -0
  416. package/wiki/Money.md +338 -0
  417. package/wiki/N-Plus-One-Detection.md +197 -0
  418. package/wiki/Notify.md +208 -0
  419. package/wiki/Observability.md +245 -0
  420. package/wiki/Outbound-HTTP.md +109 -0
  421. package/wiki/PWA-And-Offline.md +343 -0
  422. package/wiki/Policies-And-Authz.md +251 -0
  423. package/wiki/Project-Layout.md +150 -0
  424. package/wiki/Queries-And-Live-Queries.md +429 -0
  425. package/wiki/Realtime.md +734 -0
  426. package/wiki/Resource-Management.md +121 -0
  427. package/wiki/Routes-And-Render-Modes.md +521 -0
  428. package/wiki/SEO.md +288 -0
  429. package/wiki/Scheduled-Tasks.md +175 -0
  430. package/wiki/Scraping.md +493 -0
  431. package/wiki/Shapes.md +63 -0
  432. package/wiki/Stability.md +97 -0
  433. package/wiki/Static-Assets.md +166 -0
  434. package/wiki/Storage-And-Uploads.md +404 -0
  435. package/wiki/Testing.md +1313 -0
  436. package/wiki/The-Eight-Primitives.md +247 -0
  437. package/wiki/Theming.md +362 -0
  438. package/wiki/Timezones-And-Dates.md +194 -0
  439. package/wiki/Troubleshooting.md +163 -0
  440. package/wiki/Tutorial-01-First-App.md +289 -0
  441. package/wiki/Tutorial-02-First-Feature.md +339 -0
  442. package/wiki/Tutorial-03-Auth-And-Admin.md +379 -0
  443. package/wiki/Tutorial-04-Jobs-And-Realtime.md +263 -0
  444. package/wiki/Tutorial-05-Deploy-Free.md +231 -0
  445. package/wiki/Tutorial-06-Growing-Up.md +158 -0
  446. package/wiki/UI-Components.md +156 -0
  447. package/wiki/Upgrading.md +3667 -0
  448. package/src/stored-object-headers.ts +0 -43
@@ -0,0 +1,43 @@
1
+ // The checkout's top-level directories that no workspace owns or contains — in this repository,
2
+ // `scripts/`: tests that import packages by relative path and declare no dependency on any, so the
3
+ // workspace graph cannot say which change reaches them. `affected.ts` runs them for every change
4
+ // that selects a workspace at all.
5
+
6
+ // why: Bun ships no directory listing with file types.
7
+ import { readdir } from 'node:fs/promises';
8
+ import type { WorkspaceNode } from './workspace-graph';
9
+
10
+ /** Never a home for a test the run should select: dependencies, git, build state. */
11
+ const SKIPPED = (name: string): boolean => name.startsWith('.') || name === 'node_modules';
12
+
13
+ /** A root-level workspace (`''` or `.`) owns the whole checkout. */
14
+ const isRoot = (dir: string): boolean => dir === '' || dir === '.';
15
+
16
+ /**
17
+ * Pure: `entries` (top-level directory names) less any that is, or holds, a workspace — sorted.
18
+ * A directory that holds one (`packages/`, `examples/`) is answered for by the graph.
19
+ */
20
+ export function unownedDirs(
21
+ graph: readonly WorkspaceNode[],
22
+ entries: readonly string[],
23
+ ): readonly string[] {
24
+ if (graph.some((node) => isRoot(node.dir))) return [];
25
+ return entries
26
+ .filter((entry) => !SKIPPED(entry))
27
+ .filter(
28
+ (entry) => !graph.some((node) => node.dir === entry || node.dir.startsWith(`${entry}/`)),
29
+ )
30
+ .sort();
31
+ }
32
+
33
+ /** The same, read off the checkout root's own directory listing. */
34
+ export async function readUnownedDirs(
35
+ root: string,
36
+ graph: readonly WorkspaceNode[],
37
+ ): Promise<readonly string[]> {
38
+ const listed = await readdir(root, { withFileTypes: true });
39
+ return unownedDirs(
40
+ graph,
41
+ listed.filter((entry) => entry.isDirectory()).map((entry) => entry.name),
42
+ );
43
+ }
package/src/affected.ts CHANGED
@@ -13,6 +13,7 @@
13
13
  // directory is checkout-relative while a caller's scan yields paths relative to its own root.
14
14
  import { join, relative } from 'node:path';
15
15
  import { singleLine, UltimateError } from '@ultimat3/core';
16
+ import { readUnownedDirs } from './affected-unowned';
16
17
  import { BadFlagError } from './errors';
17
18
  import type { ExecResult, Runner } from './exec';
18
19
  import { execOutput } from './exec';
@@ -99,8 +100,21 @@ export interface AffectedPlan {
99
100
  */
100
101
  readonly rootWide: readonly string[];
101
102
  readonly workspaces: readonly WorkspaceNode[];
103
+ /**
104
+ * The root directories no workspace owns (`affected-unowned.ts`) — `scripts/` here — selected
105
+ * whenever any workspace is: their tests import packages by relative path, declare no
106
+ * dependency, and so are reached by a change the graph has no edge for. Empty when nothing is
107
+ * affected, and when the plan is root-wide (which already selects them).
108
+ */
109
+ readonly unowned: readonly string[];
102
110
  }
103
111
 
112
+ /**
113
+ * The CLI is the tool every test run goes through — `x test`, the gate's steps, the preloads — so a
114
+ * change to its own source is a change to how every workspace is checked, not to one workspace.
115
+ */
116
+ export const TOOL_WORKSPACE = '@ultimat3/cli';
117
+
104
118
  const byName = (a: WorkspaceNode, b: WorkspaceNode): number => (a.name > b.name ? 1 : -1);
105
119
 
106
120
  /**
@@ -110,6 +124,7 @@ const byName = (a: WorkspaceNode, b: WorkspaceNode): number => (a.name > b.name
110
124
  export function planAffected(
111
125
  graph: readonly WorkspaceNode[],
112
126
  changed: readonly string[],
127
+ unownedDirs: readonly string[] = [],
113
128
  ): AffectedPlan {
114
129
  const ignored = changed.filter(isDoc);
115
130
  const considered = changed.filter((path) => !isDoc(path));
@@ -117,18 +132,20 @@ export function planAffected(
117
132
  const seeds = new Set<string>();
118
133
  for (const path of considered) {
119
134
  const node = owningWorkspace(graph, path);
120
- if (node === undefined) rootWide.push(path);
135
+ if (node === undefined || node.name === TOOL_WORKSPACE) rootWide.push(path);
121
136
  else seeds.add(node.name);
122
137
  }
123
138
  if (rootWide.length > 0) {
124
- return { changed, ignored, rootWide, workspaces: [...graph].sort(byName) };
139
+ return { changed, ignored, rootWide, workspaces: [...graph].sort(byName), unowned: [] };
125
140
  }
126
141
  const reached = withDependents(graph, seeds);
142
+ const workspaces = graph.filter((node) => reached.has(node.name)).sort(byName);
127
143
  return {
128
144
  changed,
129
145
  ignored,
130
146
  rootWide,
131
- workspaces: graph.filter((node) => reached.has(node.name)).sort(byName),
147
+ workspaces,
148
+ unowned: workspaces.length === 0 ? [] : [...unownedDirs],
132
149
  };
133
150
  }
134
151
 
@@ -266,8 +283,8 @@ export interface AffectedScope {
266
283
  export const scopePrefixes = (plan: AffectedPlan, root: string, cwd: string): readonly string[] =>
267
284
  plan.rootWide.length > 0
268
285
  ? ['']
269
- : plan.workspaces
270
- .map((workspace) => relative(cwd, join(root, workspace.dir)).split('\\').join('/'))
286
+ : [...plan.workspaces.map((workspace) => workspace.dir), ...plan.unowned]
287
+ .map((dir) => relative(cwd, join(root, dir)).split('\\').join('/'))
271
288
  .filter((path) => !path.startsWith('..'));
272
289
 
273
290
  export const inScope = (path: string, prefixes: readonly string[]): boolean =>
@@ -290,9 +307,11 @@ export interface AffectedScopeOptions {
290
307
  export async function affectedScope(options: AffectedScopeOptions): Promise<AffectedScope> {
291
308
  const selection = readAffectedSelection(options.args, options.command);
292
309
  const root = await gitRoot(options.runner, options.cwd, options.command);
310
+ const graph = await readWorkspaceGraph(root);
293
311
  const plan = planAffected(
294
- await readWorkspaceGraph(root),
312
+ graph,
295
313
  await changedFiles(options.runner, { cwd: root, command: options.command, selection }),
314
+ await readUnownedDirs(root, graph),
296
315
  );
297
316
  return {
298
317
  selection,
@@ -35,6 +35,8 @@ export const BOUNDARY_CODES = [
35
35
  'X_BOUNDARY_ROUTE_TO_DB',
36
36
  'X_BOUNDARY_SERVICE_TO_HTTP',
37
37
  'X_BOUNDARY_SURFACE_IMPORT',
38
+ // `package-boundaries.ts`'s: it reads `packages/`, which this module's surface graph never holds.
39
+ 'X_BOUNDARY_PACKAGE_TO_APP',
38
40
  ] as const;
39
41
 
40
42
  export type BoundaryCode = (typeof BOUNDARY_CODES)[number];
@@ -192,6 +194,32 @@ const generate = (kind: 'query' | 'action', path: string, then: string): string
192
194
  return `x g ${kind} ${named} # ${then} ${commentSafe(path)}`;
193
195
  };
194
196
 
197
+ /** The slice a route's import lands in, or `undefined` for a package like `@acme/db`. */
198
+ const sliceOfImport = (path: string, specifier: string): string | undefined => {
199
+ // Resolved already where `scan` knew the file, and still relative where it did not.
200
+ const target = specifier.startsWith('.') ? normalize(join(dirname(path), specifier)) : specifier;
201
+ const [, surface, slice, rest] = /^apps\/[^/]+\/([^/]+)\/([^/]+)\/(.+)$/.exec(target) ?? [];
202
+ return surface !== undefined && SURFACE_NAMES.has(surface) && rest !== undefined
203
+ ? slice
204
+ : undefined;
205
+ };
206
+
207
+ /**
208
+ * A read through a database import. Into a slice, the query belongs there: without `--feature` a
209
+ * singular name wrote 14 files, a new entity and a table nobody asked for. A route named for its
210
+ * slice takes `<slice>-read`, since a query named as the entity collides with its import. With no
211
+ * slice to read, a new one is what the fix has to name.
212
+ */
213
+ const routeToDbFix = (path: string, specifier: string): string => {
214
+ const then = `then call it from ${commentSafe(path)}`;
215
+ const subject = subjectOf(path);
216
+ const named = subject === undefined ? '<name>' : quoteArg(subject);
217
+ const slice = sliceOfImport(path, specifier);
218
+ if (slice === undefined) return `x g resource ${named} # ${then}`;
219
+ const query = subject === slice || subject === `${slice}s` ? quoteArg(`${slice}-read`) : named;
220
+ return `x g query ${query} --feature ${quoteArg(slice)} # ${then}`;
221
+ };
222
+
195
223
  /**
196
224
  * Both fixes are one runnable line, with the rest of the instruction behind a `#` — a fix a
197
225
  * caller has to edit before it runs is prose, and prose is what "errors are instructions" bans.
@@ -204,7 +232,7 @@ function layerFindings(scanned: readonly ScannedFile[]): readonly Finding[] {
204
232
  findings.push({
205
233
  code: 'X_BOUNDARY_ROUTE_TO_DB',
206
234
  cause: `route imports the database ("${specifier}")${isRepoSpecifier(specifier) ? ', through a repo' : ''} — routes call actions and queries`,
207
- fix: generate('query', file.path, 'then call it from'),
235
+ fix: routeToDbFix(file.path, specifier),
208
236
  docs: ERROR_DOCS_URL,
209
237
  at: file.path,
210
238
  });
package/src/app-evals.ts CHANGED
@@ -7,6 +7,7 @@
7
7
 
8
8
  import {
9
9
  baselinePath,
10
+ describeAgents,
10
11
  describeEvals,
11
12
  EvalBaselineMissingError,
12
13
  EvalMissingError,
@@ -36,10 +37,17 @@ export async function checkEvalCoverage(root: string): Promise<readonly Finding[
36
37
  // Loading is idempotent per process: `x verify` loads the app once and every later step,
37
38
  // including the manifest, reads the same registries.
38
39
  await loadApp(root);
39
- return promptsWithoutEvals().map((prompt) => ({
40
- ...findingFrom(new EvalMissingError({ prompt: prompt.ref, id: prompt.id })),
41
- at: prompt.ref,
42
- }));
40
+ // An agent's prompt is measured by the agent arm — its tools and turns — so the fix names it.
41
+ const agents = new Map(describeAgents().map((fact) => [fact.prompt, fact.name]));
42
+ return promptsWithoutEvals().map((prompt) => {
43
+ const agent = agents.get(prompt.ref);
44
+ const missing = new EvalMissingError({
45
+ prompt: prompt.ref,
46
+ id: prompt.id,
47
+ ...(agent === undefined ? {} : { agent }),
48
+ });
49
+ return { ...findingFrom(missing), at: prompt.ref };
50
+ });
43
51
  }
44
52
 
45
53
  /**
@@ -0,0 +1,62 @@
1
+ // A module that imports what does not resolve. It was `X_CLI_UNEXPECTED`, once per module reaching
2
+ // it, with `x doctor --json` as the fix, and doctor answered "shippable": the cycle closed on itself.
3
+ // Leaf module: `output.ts` reads it for a throw outside the scan, and the scan for one inside it.
4
+
5
+ // why: a synchronous probe for an installed package's manifest; Bun ships no path helpers.
6
+ import { existsSync } from 'node:fs';
7
+ import { dirname, join } from 'node:path';
8
+ import { stringField } from '@ultimat3/core';
9
+ import type { Finding } from './output';
10
+ import { posixRelative } from './posix-path';
11
+ import { quoteArg } from './shell-quote';
12
+
13
+ export const IMPORT_UNRESOLVED = 'X_APP_IMPORT_UNRESOLVED';
14
+
15
+ /** npm's own shape, so `@/lib/x` (a path alias) and `node:nope` are not offered to `bun add`. */
16
+ const PACKAGE_NAME = /^(?:@[a-z0-9~-][a-z0-9._~-]*\/)?[a-z0-9~-][a-z0-9._~-]*/;
17
+
18
+ /** `@scope/pkg/sub` → `@scope/pkg`, or `undefined` for what no install can resolve. */
19
+ export const installablePackage = (specifier: string): string | undefined => {
20
+ const name = PACKAGE_NAME.exec(specifier)?.[0];
21
+ if (name === undefined) return undefined;
22
+ const rest = specifier.slice(name.length);
23
+ return rest === '' || rest.startsWith('/') ? name : undefined;
24
+ };
25
+
26
+ /** Is `pkg` in a `node_modules` the referrer resolves through, as the resolver walks: up to `/`. */
27
+ const isInstalled = (pkg: string, referrer: string): boolean => {
28
+ for (let dir = dirname(referrer); ; dir = dirname(dir)) {
29
+ if (existsSync(join(dir, 'node_modules', pkg, 'package.json'))) return true;
30
+ if (dirname(dir) === dir) return false;
31
+ }
32
+ };
33
+
34
+ /**
35
+ * Bun's `ResolveMessage`, read through `stringField`: a caught value this process did not build. The
36
+ * `specifier` and `referrer` are the failing import's own, however many modules were importing the
37
+ * file that holds it, which is what makes one finding per specifier true.
38
+ */
39
+ export function unresolvedImportFinding(error: unknown, root: string): Finding | undefined {
40
+ if (stringField(error, 'code') !== 'ERR_MODULE_NOT_FOUND') return undefined;
41
+ const specifier = stringField(error, 'specifier');
42
+ if (specifier === undefined) return undefined;
43
+ const referrer = stringField(error, 'referrer');
44
+ const at = referrer === undefined ? undefined : posixRelative(root, referrer);
45
+ const where = at ?? 'an app module';
46
+ const named = installablePackage(specifier);
47
+ // Installed, and missing only the subpath: `bun add` would exit 0 and change nothing.
48
+ const installed = named !== undefined && referrer !== undefined && isInstalled(named, referrer);
49
+ const pkg = installed ? undefined : named;
50
+ const edit = `grep -nF -- ${quoteArg(specifier)} ${quoteArg(where)} # the import no file resolves: edit it`;
51
+ return {
52
+ code: IMPORT_UNRESOLVED,
53
+ cause: installed
54
+ ? `${where} imports "${specifier}", and ${String(named)} is installed with no such subpath`
55
+ : pkg === undefined
56
+ ? `${where} imports "${specifier}", which resolves to no file`
57
+ : `an app module imports ${pkg}, which is not installed`,
58
+ fix: pkg === undefined ? edit : `bun add ${quoteArg(pkg)}`,
59
+ ...(at === undefined ? {} : { at }),
60
+ meta: { specifier },
61
+ };
62
+ }
package/src/app-load.ts CHANGED
@@ -29,6 +29,7 @@ import {
29
29
  // accident one refactor away from compiling every app's `.tsx` to `React.createElement`. The named
30
30
  // import is a value import, so the side effect holds without the bare line it replaced.
31
31
  import { setStylesheetRoot } from '@ultimat3/render/server';
32
+ import { unresolvedImportFinding } from './app-load-unresolved';
32
33
  import {
33
34
  evictChanged,
34
35
  pinModule,
@@ -61,6 +62,13 @@ const APP_GLOBS = [
61
62
  */
62
63
  const ENTRY_POINT = /^apps\/[^/]+\/(?:server|prerender)\.tsx?$/;
63
64
 
65
+ /**
66
+ * `apps/<app>/exercise.ts` is `x exercise`'s input, read by that command alone. It imports
67
+ * `@ultimat3/testing`, a devDependency a production image does not install, so the boot scan that
68
+ * imported it would fail the module on every start.
69
+ */
70
+ const EXERCISE_FILE = /^apps\/[^/]+\/exercise\.ts$/;
71
+
64
72
  /**
65
73
  * A `*.island.tsx` is a CLIENT entry point and is deliberately not imported here. It registers no
66
74
  * primitive — a page names it by specifier, never by import — and importing it would put the one
@@ -138,13 +146,13 @@ export function sortModulePaths(paths: readonly string[]): string[] {
138
146
  * Every module path the app globs match, SORTED — with the API index first. `Bun.Glob` answers in
139
147
  * directory order, which is the filesystem's — ext4 hashes names with a per-filesystem seed — so
140
148
  * two pods of one image imported the app in two orders, registered its stylesheets in two orders,
141
- * and served two different `/styles/<hash>.css` for one page (notificado.co, 22.3.2). Import order
149
+ * and served two different `/styles/<hash>.css` for one page (one adopter, 22.3.2). Import order
142
150
  * is the stylesheet cascade's order, so it must be the same everywhere.
143
151
  *
144
152
  * The API index leads because `defineApi({ http: { pathStyle } })` is what every action URL is
145
153
  * derived under: sorted, `apps/admin/**` evaluated before it, and a module-level
146
154
  * `derivePath('signOut').path` there captured the default style's `/api/outs/sign` — a form
147
- * posting to a URL no route served (notificado.co, 22.6.0). Anything still evaluated before the
155
+ * posting to a URL no route served (one adopter, 22.6.0). Anything still evaluated before the
148
156
  * declaration (the index's own imports) is refused at it, `X_ACTION_PATH_DERIVED_EARLY`.
149
157
  */
150
158
  export async function appModulePaths(root: string): Promise<readonly string[]> {
@@ -211,32 +219,33 @@ export async function scanAppModules(root: string, options: ScanOptions): Promis
211
219
  // `~/work/my.test.app` matched `.test.` on every file and loaded none of them.
212
220
  const file = posixRelative(root, absolute);
213
221
  if (hasPathSegment(absolute, 'node_modules') || isTest(file)) continue;
214
- if (ENTRY_POINT.test(file) || CLIENT_ENTRY_POINT.test(file) || STATES_FILE.test(file)) {
222
+ if (
223
+ ENTRY_POINT.test(file) ||
224
+ EXERCISE_FILE.test(file) ||
225
+ CLIENT_ENTRY_POINT.test(file) ||
226
+ STATES_FILE.test(file)
227
+ ) {
215
228
  continue;
216
229
  }
217
230
  if (options.include !== undefined && !(await options.include(absolute))) continue;
218
- // The source is read BEFORE the import, on the file's first pass and for every route module —
219
- // a rescan of any other registered module reads nothing here. What is recorded is bound to the
220
- // bytes the module was evaluated from, and a read AFTER the import cannot know which bytes
221
- // those were: a save landing between the two bound V1's component to V2's hash, so the next
222
- // scan saw nothing to do and served V1 until the save after. Read first, the worst case is one
223
- // re-import the next tick, of a file that did change. An untracked scan reads nothing here:
224
- // no save lands in an image, so a route module's text is read after its import, once.
231
+ // The source is read BEFORE the import, on the file's first pass — a rescan of a registered
232
+ // module reads nothing here. What is recorded is bound to the bytes the module was evaluated
233
+ // from, and a read AFTER the import cannot know which bytes those were: a save landing between
234
+ // the two bound V1's component to V2's hash, so the next scan saw nothing to do and served V1
235
+ // until the save after. Read first, the worst case is one re-import the next tick, of a file
236
+ // that did change. An untracked scan reads nothing here: no save lands in an image.
225
237
  const first = !registered.has(absolute) && !failures.has(absolute);
226
- const snapshot =
227
- options.track && (first || routeModules.has(absolute))
228
- ? await Bun.file(absolute).text()
229
- : undefined;
238
+ const snapshot = options.track && first ? await Bun.file(absolute).text() : undefined;
230
239
  if (first && snapshot !== undefined) await trackModule(absolute, snapshot, root);
231
240
  let module: Record<string, unknown>;
232
241
  try {
233
242
  module = (await import(absolute)) as Record<string, unknown>;
234
243
  } catch (error) {
235
- findings.push({ ...findingFrom(error), at: file });
244
+ findings.push(unresolvedImportFinding(error, root) ?? { ...findingFrom(error), at: file });
236
245
  continue;
237
246
  }
238
247
  files.push(file);
239
- const finding = await register(absolute, file, module, snapshot, options.track);
248
+ const finding = await register(absolute, file, module, options.track);
240
249
  if (finding !== undefined) findings.push(finding);
241
250
  }
242
251
 
@@ -303,25 +312,22 @@ const emptyAppFinding = (root: string): Finding => ({
303
312
  /**
304
313
  * Registers a module once; every later call replays whatever the first one reported — except for
305
314
  * a route module, which a later call re-registers when the import handed back a new instance.
306
- * `snapshot` is the source read before this import: always for a first pass and a route module of
307
- * a TRACKED scan. An untracked one hands none, and a route module's text is read here instead.
308
315
  */
309
316
  async function register(
310
317
  absolute: string,
311
318
  file: string,
312
319
  module: Record<string, unknown>,
313
- snapshot: string | undefined,
314
320
  track: boolean,
315
321
  ): Promise<Finding | undefined> {
316
322
  const previous = failures.get(absolute);
317
323
  if (previous !== undefined) return previous;
318
- if (registered.has(absolute)) return reloadRoute(absolute, file, module, snapshot);
324
+ if (registered.has(absolute)) return track ? reloadRoute(absolute, file, module) : undefined;
319
325
  registered.add(absolute);
320
326
  try {
321
327
  const config = module['config'];
322
328
  const route = isRouteConfig(config) ? config : undefined;
323
329
  if (route !== undefined) {
324
- registerRouteModule(file, module, route, snapshot ?? (await Bun.file(absolute).text()));
330
+ registerRouteModule(file, module, route);
325
331
  routeModules.set(absolute, module);
326
332
  }
327
333
  // A pin is the reload graph's: it stops an eviction a process that never rescans never runs,
@@ -364,18 +370,11 @@ function definesPrimitive(module: Record<string, unknown>, route: boolean): bool
364
370
  });
365
371
  }
366
372
 
367
- /**
368
- * The route half of a registration, and the whole of a re-registration. `source` is the text the
369
- * module was imported from — read by the caller BEFORE the import, never here after it — and the
370
- * hash the entry is bound to is that text's. Until 2026-09-07 this read the file again, and a save
371
- * between the import and that read registered the old component under the new hash: the next
372
- * scan compared equal, and the page on disk was not served until the save after it.
373
- */
373
+ /** The route half of a registration, and the whole of a re-registration. */
374
374
  function registerRouteModule(
375
375
  file: string,
376
376
  module: Record<string, unknown>,
377
377
  config: RouteConfig,
378
- source: string,
379
378
  ): void {
380
379
  // The page component comes from the same module as its config, resolved by render's own
381
380
  // rule — the CLI does not decide which export is a page any more than it decides what a
@@ -384,9 +383,6 @@ function registerRouteModule(
384
383
  registerRoute({
385
384
  file,
386
385
  config,
387
- // The build counts boundaries from the compiled JSX; before a build there is only the
388
- // source, and `render: 'stream'` is rejected without one — so count them in the text.
389
- suspenseBoundaries: countSuspense(source),
390
386
  ...(component === undefined ? {} : { component }),
391
387
  });
392
388
  }
@@ -401,16 +397,15 @@ function reloadRoute(
401
397
  absolute: string,
402
398
  file: string,
403
399
  module: Record<string, unknown>,
404
- source: string | undefined,
405
400
  ): Finding | undefined {
406
401
  const held = routeModules.get(absolute);
407
- if (held === undefined || held === module || source === undefined) return undefined;
402
+ if (held === undefined || held === module) return undefined;
408
403
  try {
409
404
  const config = module['config'];
410
405
  // A file that stopped being a route is not un-registered — the table has no verb for it, and a
411
406
  // deleted file needs a restart either way. Its instance is recorded so the same save is not
412
407
  // re-registered on every later tick.
413
- if (isRouteConfig(config)) registerRouteModule(file, module, config, source);
408
+ if (isRouteConfig(config)) registerRouteModule(file, module, config);
414
409
  routeModules.set(absolute, module);
415
410
  return undefined;
416
411
  } catch (error) {
@@ -418,8 +413,6 @@ function reloadRoute(
418
413
  }
419
414
  }
420
415
 
421
- const countSuspense = (source: string): number => source.match(/<Suspense[\s/>]/g)?.length ?? 0;
422
-
423
416
  /**
424
417
  * `packages/db/src/errors.ts` → `packages/db`; `apps/web/app/posts/errors.ts` → `apps/web` — on
425
418
  * either separator. A manifest written on Windows split a `\`-path on `/`, kept the whole file as
package/src/app-mcp.ts CHANGED
@@ -13,12 +13,14 @@ import { existsSync } from 'node:fs';
13
13
  import { join } from 'node:path';
14
14
  import { logger } from '@ultimat3/core';
15
15
  import type { Route } from '@ultimat3/http';
16
- import { jsonResponse, selfOrigin } from '@ultimat3/http';
16
+ import { jsonResponse, originListed, selfOrigin } from '@ultimat3/http';
17
17
  import {
18
18
  type AppMcp,
19
+ assertRecipeLinks,
19
20
  McpAppUnmountedError,
20
21
  McpPathDuplicateError,
21
22
  PROTECTED_RESOURCE_WELL_KNOWN,
23
+ servedRecipes,
22
24
  } from '@ultimat3/mcp';
23
25
  import { loadAppConfig } from './app-config-load';
24
26
 
@@ -103,6 +105,9 @@ export async function appMcpMount(root: string): Promise<AppMcpMount> {
103
105
  const module = (await import(join(root, file))) as Record<string, unknown>;
104
106
  const endpoints = endpointsOf(module[APP_MCP_EXPORT]);
105
107
  if (endpoints === undefined) continue;
108
+ // Every endpoint is built by now: an error's declared `docs://recipes/` link must name a recipe
109
+ // one of them serves (`X_MCP_RECIPE_UNKNOWN`), the union and never one endpoint alone.
110
+ assertRecipeLinks(servedRecipes());
106
111
  return mountEndpoints(endpoints, file);
107
112
  }
108
113
  return {
@@ -199,8 +204,15 @@ function endpointRoutes(route: Descriptor, path: string, index: number): readonl
199
204
  route.handle(request.raw, {
200
205
  origin: selfOrigin(ctx.url, ctx.https),
201
206
  address: ctx.ip ?? undefined,
207
+ // A browser `Origin` is proven the way CSRF proves it: this origin, or one listed exactly.
208
+ listed: (origin) => originListed(ctx.config.cors, origin),
202
209
  }),
203
- meta: { name, auth: 'public', enforcedBy: 'handler' },
210
+ // `authenticate` answers nobody: the route's ONE credential is the bearer `resolveToken`
211
+ // reads, so a session cookie authenticates nothing here. Without it the app's authenticator
212
+ // made a cookie-carrying (or, under `x dev`, any) request a user's, and CSRF then refused a
213
+ // token-less MCP client 403 where it is owed the 401 and its challenge. No cookie identifies
214
+ // anyone, so there is no ambient credential for a forged request to ride.
215
+ meta: { name, auth: 'public', enforcedBy: 'handler', authenticate: () => null },
204
216
  },
205
217
  ...metadata,
206
218
  ];