@ultimat3/cli 29.3.2 → 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 +13 -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 +28 -8
  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 +42 -138
  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 +17 -27
  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,250 @@
1
+ // `x g sign-in --password | --oidc <provider>`: the shape's files written through the one writer
2
+ // (`writeFiles` — a second run conflicts and writes nothing), then the edits outside them: the
3
+ // actions listed in the API index, `app.config.ts`'s `signInPath` and env keys, and — for OIDC —
4
+ // the authenticator swap that drops `x new`'s development viewer, only over `x new`'s own bytes.
5
+
6
+ import { ERROR_DOCS_URL, stringField } from '@ultimat3/core';
7
+ import { indexBindingFindings } from './api-registration';
8
+ import { generateSpec } from './cmd-generate-spec';
9
+ import type { CommandContext } from './command';
10
+ import { invocationOf } from './command';
11
+ import { EXERCISE_FILE, exerciseWithEntries, signInExerciseEntries } from './exercise-registration';
12
+ import { localiseCatalogs, namedOrAppLocales } from './generate-catalog-locales';
13
+ import { appDisk, plannedDisk } from './generate-disk';
14
+ import { generate } from './generate-files';
15
+ import { followUpEdits } from './generate-follow-ups';
16
+ import { formatWritten } from './generate-format-written';
17
+ import { APP_CONFIG, type SignInEnvKey, signInConfigEdit } from './generate-sign-in-config';
18
+ import { appManifestIfAny, existingArtifacts } from './generate-sign-in-manifest';
19
+ import { readSignInMode, type SignInMode, signInLabel } from './generate-sign-in-mode';
20
+ import { planWrites, writeFiles } from './generate-write';
21
+ import { resolveDbModule } from './handle-registration';
22
+ import { resolveCatalogModule } from './i18n-audit';
23
+ import { reproducedFlags } from './invocation-flags';
24
+ import { msg } from './messages';
25
+ import type { CommandResult, Finding } from './output';
26
+ import { flagBool, flagList } from './parse';
27
+ import { names, resolveLocales } from './templates';
28
+ import { authFiles } from './templates/scaffold-auth';
29
+ import { DEV_ACTOR_FILES, signedInAuthenticatorFiles } from './templates/sign-in-authenticator';
30
+ import { oidcEnvPrefix } from './templates/sign-in-oidc';
31
+ import { SIGN_IN_PATH } from './templates/sign-in-shared';
32
+
33
+ /** The env keys an OIDC sign-in adds to `envSchema`; `SESSION_SECRET` and `APP_URL` exist already. */
34
+ export const oidcEnvKeys = (provider: string): readonly SignInEnvKey[] => {
35
+ const prefix = oidcEnvPrefix(provider);
36
+ return [
37
+ { key: `${prefix}_ISSUER`, type: 'url', description: `The ${provider} OpenID Connect issuer.` },
38
+ { key: `${prefix}_CLIENT_ID`, description: `The client id ${provider} issued this app.` },
39
+ {
40
+ key: `${prefix}_CLIENT_SECRET`,
41
+ secret: true,
42
+ description: `The client secret ${provider} issued this app.`,
43
+ },
44
+ ];
45
+ };
46
+
47
+ /**
48
+ * The app's slug, as `x new` named it: the root `package.json`'s `name`. A manifest that does not
49
+ * parse names nothing, which is the conservative answer: no scaffold to compare against, so every
50
+ * file on disk reads as edited and the swap becomes a finding rather than a throw.
51
+ */
52
+ export async function appName(root: string): Promise<string | undefined> {
53
+ const text = await appDisk(root).read('package.json');
54
+ if (text === undefined) return undefined;
55
+ let parsed: unknown;
56
+ try {
57
+ parsed = JSON.parse(text);
58
+ } catch {
59
+ return undefined;
60
+ }
61
+ return stringField(parsed, 'name');
62
+ }
63
+
64
+ export interface AuthenticatorSwap {
65
+ /** The files written over `x new`'s, and the ones deleted, when every one was still `x new`'s. */
66
+ readonly replace: readonly { readonly path: string; readonly contents: string }[];
67
+ readonly remove: readonly string[];
68
+ readonly findings: readonly Finding[];
69
+ }
70
+
71
+ /** Whether `x new`'s authenticator and development viewer are on disk exactly as it wrote them. */
72
+ export async function planAuthenticatorSwap(root: string): Promise<AuthenticatorSwap> {
73
+ const name = await appName(root);
74
+ const scaffold = new Map(
75
+ name === undefined ? [] : authFiles(names(name)).map((file) => [file.path, file.contents]),
76
+ );
77
+ const swapped = signedInAuthenticatorFiles();
78
+ const paths = [...swapped.map((file) => file.path), ...DEV_ACTOR_FILES];
79
+ const disk = appDisk(root);
80
+ const edited: string[] = [];
81
+ for (const path of paths) {
82
+ const now = await disk.read(path);
83
+ if (now !== undefined && now !== scaffold.get(path)) edited.push(path);
84
+ }
85
+ if (edited.length > 0) {
86
+ return {
87
+ replace: [],
88
+ remove: [],
89
+ findings: [
90
+ {
91
+ code: 'X_GENERATE_CONFLICT',
92
+ cause: `${edited.join(', ')} ${edited.length === 1 ? 'is' : 'are'} no longer what x new wrote, so the development viewer was left in place rather than overwritten`,
93
+ fix: 'rm apps/web/app/auth/dev-actor.ts apps/web/app/auth/dev-actor.test.ts # then answer null in apps/web/app/auth/authenticator.ts where it called devAuthenticate(request)',
94
+ docs: ERROR_DOCS_URL,
95
+ at: edited[0] ?? '',
96
+ },
97
+ ],
98
+ };
99
+ }
100
+ return {
101
+ replace: swapped.map((file) => ({ path: file.path, contents: String(file.contents) })),
102
+ remove: [...DEV_ACTOR_FILES],
103
+ findings: [],
104
+ };
105
+ }
106
+
107
+ export async function runSignIn(ctx: CommandContext, root: string): Promise<CommandResult> {
108
+ const mode: SignInMode = readSignInMode(ctx.args);
109
+ const label = signInLabel(mode);
110
+ const catalogModule = await resolveCatalogModule(root);
111
+ const dbModule = await resolveDbModule(root);
112
+ // Every catalog the app keeps, as every other generator seeds them: `en` alone renders ⟦key⟧
113
+ // for a reader of any other locale the app ships.
114
+ const locales = resolveLocales(await namedOrAppLocales(root, flagList(ctx.args, 'locales')));
115
+ const files = await localiseCatalogs(
116
+ root,
117
+ generate({
118
+ kind: 'sign-in',
119
+ name: label,
120
+ signIn: mode,
121
+ locales,
122
+ ...(catalogModule === undefined ? {} : { catalogModule }),
123
+ }),
124
+ );
125
+ const flags = reproducedFlags(generateSpec, ctx.args);
126
+ const invocation = [invocationOf(ctx, 'g'), 'sign-in', ...flags].join(' ');
127
+ const force = flagBool(ctx.args, 'force');
128
+ const swap =
129
+ mode.kind === 'oidc'
130
+ ? await planAuthenticatorSwap(root)
131
+ : { replace: [], remove: [], findings: [] };
132
+ const configSource = await appDisk(root).read(APP_CONFIG);
133
+ const config = signInConfigEdit(
134
+ configSource ?? '',
135
+ SIGN_IN_PATH,
136
+ mode.kind === 'oidc' ? oidcEnvKeys(mode.provider) : [],
137
+ );
138
+ // An app with no `app.config.ts` has no `signInPath` to declare: nothing to edit, nothing owed.
139
+ const configFindings = configSource === undefined ? [] : config.findings;
140
+ const configEdited = configSource !== undefined && config.contents !== configSource;
141
+ // The three actions are listed in the API index under their file names, so an app already
142
+ // binding `createSession` there is refused before anything lands — `cmd-generate.ts`'s preflight.
143
+ const bindings = await indexBindingFindings(
144
+ root,
145
+ files.map((file) => file.path),
146
+ `${invocation} # after renaming what apps/web/api/index.ts already binds under that name`,
147
+ );
148
+
149
+ // The pass's half: what the written actions cannot run as generated, turned off with a reason.
150
+ const exerciseSource = await appDisk(root).read(EXERCISE_FILE);
151
+ const exerciseContents = exerciseWithEntries(
152
+ exerciseSource,
153
+ signInExerciseEntries(files.map((file) => file.path)),
154
+ );
155
+ const exerciseEdited = exerciseContents !== exerciseSource;
156
+
157
+ if (flagBool(ctx.args, 'dry-run')) {
158
+ const plan = await planWrites(root, files, force, invocation, label);
159
+ const planned = new Map(
160
+ files.flatMap((file) =>
161
+ plan.written.includes(file.path) && typeof file.contents === 'string'
162
+ ? [[file.path, file.contents] as const]
163
+ : [],
164
+ ),
165
+ );
166
+ const refused = [...plan.conflicts, ...bindings];
167
+ const follow =
168
+ refused.length > 0
169
+ ? { edited: [], findings: [] }
170
+ : await followUpEdits(root, plan.written, dbModule, plannedDisk(root, planned));
171
+ const findings = [...refused, ...follow.findings, ...swap.findings, ...configFindings];
172
+ const touched =
173
+ refused.length > 0
174
+ ? []
175
+ : [
176
+ ...new Set([
177
+ ...plan.written,
178
+ ...follow.edited,
179
+ ...swap.replace.map((file) => file.path),
180
+ ...(configEdited ? [APP_CONFIG] : []),
181
+ ...(exerciseEdited ? [EXERCISE_FILE] : []),
182
+ ]),
183
+ ];
184
+ const removed = refused.length > 0 ? [] : [...swap.remove];
185
+ return {
186
+ ok: findings.length === 0,
187
+ command: 'g',
188
+ summary: msg('cli.generate.planned', { count: touched.length, kind: 'sign-in', name: label }),
189
+ data: {
190
+ files: touched,
191
+ removed,
192
+ // What a real run re-projects after it writes, named as `x g`'s other dry runs name them.
193
+ ...(touched.length === 0 ? {} : { refreshes: [...(await existingArtifacts(root))] }),
194
+ dryRun: true,
195
+ },
196
+ lines: fileLines(touched, removed),
197
+ findings,
198
+ };
199
+ }
200
+
201
+ const report =
202
+ bindings.length > 0
203
+ ? { written: [], conflicts: bindings }
204
+ : await writeFiles(root, files, force, invocation, label);
205
+ if (report.conflicts.length > 0) {
206
+ return {
207
+ ok: false,
208
+ command: 'g',
209
+ summary: msg('cli.generate.wrote', { count: 0, kind: 'sign-in', name: label }),
210
+ data: { files: [] },
211
+ findings: report.conflicts,
212
+ };
213
+ }
214
+ const disk = appDisk(root);
215
+ for (const file of swap.replace) await disk.write(file.path, file.contents);
216
+ for (const path of swap.remove) {
217
+ const file = Bun.file(`${root}/${path}`);
218
+ if (await file.exists()) await file.delete();
219
+ }
220
+ if (configEdited) await disk.write(APP_CONFIG, config.contents);
221
+ if (exerciseEdited) await disk.write(EXERCISE_FILE, exerciseContents);
222
+ const follow = await followUpEdits(root, report.written, dbModule, disk);
223
+ await formatWritten(root, report.written, ctx.runner);
224
+ const refreshed = await appManifestIfAny(root);
225
+ const written = [
226
+ ...new Set([
227
+ ...report.written,
228
+ ...swap.replace.map((file) => file.path),
229
+ ...(configEdited ? [APP_CONFIG] : []),
230
+ ...(exerciseEdited ? [EXERCISE_FILE] : []),
231
+ ...follow.edited,
232
+ ...refreshed.written,
233
+ ]),
234
+ ];
235
+ const findings = [...follow.findings, ...swap.findings, ...configFindings, ...refreshed.findings];
236
+ return {
237
+ ok: findings.length === 0,
238
+ command: 'g',
239
+ summary: msg('cli.generate.wrote', { count: written.length, kind: 'sign-in', name: label }),
240
+ data: { files: written, removed: [...swap.remove] },
241
+ lines: fileLines(written, swap.remove),
242
+ findings,
243
+ };
244
+ }
245
+
246
+ /** One `+` line per file written, then one `-` per file deleted: a deletion is never "added". */
247
+ const fileLines = (added: readonly string[], removed: readonly string[]): readonly string[] => [
248
+ ...added.map((path) => msg('cli.file.added', { path })),
249
+ ...removed.map((path) => msg('cli.file.removed', { path })),
250
+ ];
@@ -3,14 +3,17 @@
3
3
  // which decides WHICH files a generator emits — this file decides what happens to them, and `x new`
4
4
  // and the scaffold fixture assemble their own lists and land them through exactly these rules.
5
5
 
6
- // `resolve`/`sep` and not `join`: only resolving the assembled path can prove it stayed inside the
7
- // app root, and `node:path` is the only API that resolves one. `node:fs` for the exists check.
6
+ // why: `resolve`/`sep` and not `join`: only resolving the assembled path can prove it stayed
7
+ // inside the app root, and `node:path` is the only API that resolves one. `node:fs` for the exists
8
+ // check.
8
9
  import { existsSync } from 'node:fs';
9
- import { resolve, sep } from 'node:path';
10
+ import { dirname, join, resolve, sep } from 'node:path';
10
11
  import { ERROR_DOCS_URL } from '@ultimat3/core';
12
+ import { commentSafe } from './comment-safe';
11
13
  import { GenerateJsonInvalidError, ScaffoldPathEscapeError } from './errors';
12
14
  import { mergeJsonDeep, mergeJsonInPlace, sortJsonDeep } from './json-merge';
13
15
  import type { Finding } from './output';
16
+ import { quoteArg } from './shell-quote';
14
17
  import type { GeneratedFile } from './templates';
15
18
 
16
19
  /** `undefined` when `text` does not parse as a JSON object — the one shape every catalog, whether
@@ -164,11 +167,40 @@ type WritePlan =
164
167
  | { readonly kind: 'skip' }
165
168
  | { readonly kind: 'conflict'; readonly finding: Finding };
166
169
 
170
+ /**
171
+ * The author's rules live in a slice's `entity.ts` and `policy.ts`: `--force` as the first thing
172
+ * offered deleted them, when an existing slice almost always means the next primitive belongs in it.
173
+ */
174
+ const conflictFix = (
175
+ path: string,
176
+ absolute: string,
177
+ invocation: string,
178
+ name: string | undefined,
179
+ ): string => {
180
+ const [slice, module] = path.split('/').slice(-2);
181
+ // The caller's own name, never `<name>`: a placeholder pasted into a shell is a redirect. With no
182
+ // name to hand back, the one line that needs none.
183
+ if (
184
+ slice === undefined ||
185
+ name === undefined ||
186
+ (module !== 'entity.ts' && module !== 'policy.ts')
187
+ )
188
+ return `${invocation} --force # overwrites ${path}, or pass a different name`;
189
+ const into = `--feature ${quoteArg(slice)}`;
190
+ const named = quoteArg(name);
191
+ // A read needs the slice's table (X_FEATURE_REQUIRED), so it is offered only beside one.
192
+ const read = existsSync(join(dirname(absolute), 'entity.ts'))
193
+ ? `or x g query ${named} ${into}`
194
+ : `a read needs x g entity ${quoteArg(slice)} first`;
195
+ return `x g action ${named} ${into} # ${read}: ${commentSafe(path)} is the author's`;
196
+ };
197
+
167
198
  function planFile(
168
199
  file: GeneratedFile,
169
200
  absolute: string,
170
201
  force: boolean,
171
202
  invocation: string,
203
+ name: string | undefined,
172
204
  ): WritePlan {
173
205
  // A foundation file belongs to the slice, not to the generator that needs it: several generators
174
206
  // emit the same `repo.ts`, so an existing one is the author's — never a conflict, and never
@@ -189,7 +221,7 @@ function planFile(
189
221
  // The caller's own invocation, not `x g <kind>`: `x g --force` is X_CLI_UNKNOWN_COMMAND
190
222
  // when run, and a `fix:` is copied and pasted verbatim. Same construction as
191
223
  // `generate-kinds.ts`'s `assertSurfaceSupported`.
192
- fix: `${invocation} --force # overwrites ${file.path}, or pass a different name`,
224
+ fix: conflictFix(file.path, absolute, invocation, name),
193
225
  docs: ERROR_DOCS_URL,
194
226
  at: file.path,
195
227
  },
@@ -204,6 +236,7 @@ async function planAll(
204
236
  files: readonly GeneratedFile[],
205
237
  force: boolean,
206
238
  invocation: string,
239
+ name: string | undefined,
207
240
  ): Promise<readonly WritePlan[]> {
208
241
  const plans: WritePlan[] = [];
209
242
  for (const file of files) {
@@ -211,7 +244,7 @@ async function planAll(
211
244
  plans.push(
212
245
  file.merge === 'json'
213
246
  ? await planJsonMerge(file, absolute)
214
- : planFile(file, absolute, force, invocation),
247
+ : planFile(file, absolute, force, invocation, name),
215
248
  );
216
249
  }
217
250
  return plans;
@@ -230,8 +263,10 @@ export async function planWrites(
230
263
  files: readonly GeneratedFile[],
231
264
  force: boolean,
232
265
  invocation = 'x g <kind> <name>',
266
+ /** The name the caller gave, handed back in a conflict's fix; absent, that fix names none. */
267
+ name?: string,
233
268
  ): Promise<WriteReport> {
234
- const plans = await planAll(root, files, force, invocation);
269
+ const plans = await planAll(root, files, force, invocation, name);
235
270
  const conflicts = conflictsOf(plans);
236
271
  if (conflicts.length > 0) return { written: [], conflicts };
237
272
  return {
@@ -259,8 +294,10 @@ export async function writeFiles(
259
294
  * shape, not a runnable line, and every generator path supplies the real one.
260
295
  */
261
296
  invocation = 'x g <kind> <name>',
297
+ /** `planWrites`' `name`. */
298
+ name?: string,
262
299
  ): Promise<WriteReport> {
263
- const plans = await planAll(root, files, force, invocation);
300
+ const plans = await planAll(root, files, force, invocation, name);
264
301
  const conflicts = conflictsOf(plans);
265
302
  if (conflicts.length > 0) return { written: [], conflicts };
266
303
 
@@ -0,0 +1,72 @@
1
+ // An app's own websocket upgrade, made a build error: `deviceSocket()` is the one way an app serves
2
+ // a long-lived socket, and a hand-rolled `server.upgrade(` or `Bun.serve({ websocket })` skips the
3
+ // token check before the handshake, the per-actor cap, the beat, the frame limit and the drain.
4
+ // Pure over the `boundaries` step's one read; masked, so a comment or a string never matches.
5
+
6
+ import { ERROR_DOCS_URL, maskLiterals } from '@ultimat3/core';
7
+ import type { SourceFile } from './app-boundaries';
8
+ import { commentSafe } from './comment-safe';
9
+ import type { Finding } from './output';
10
+
11
+ /** `Bun.serve(` — its argument is read for a `websocket` key. */
12
+ const SERVE = /\bBun\s*\.\s*serve\s*\(/g;
13
+ /** A `websocket` KEY in an object literal: `websocket: x`, or the shorthand `{ websocket }`. */
14
+ const WEBSOCKET_KEY = /\bwebsocket\s*[:,}]/;
15
+ /**
16
+ * Bun's upgrade, by the names its two halves carry: the server (`server.upgrade(`, `srv.upgrade(`)
17
+ * or the request it takes (`x.upgrade(request`). `plans.upgrade(org)` is somebody's business.
18
+ */
19
+ const UPGRADE = /\b(?:server|srv)\s*\.\s*upgrade\s*\(|\.\s*upgrade\s*\(\s*(?:req|request)\b/g;
20
+ /** An inline handler set — `websocket: {` — whatever server it is handed to. */
21
+ const HANDLERS = /\bwebsocket\s*:\s*\{/g;
22
+
23
+ /** The index closing the `(` at `open`, or the end of the text. Exact over `maskLiterals` output. */
24
+ function closeOf(masked: string, open: number): number {
25
+ let depth = 0;
26
+ for (let index = open; index < masked.length; index += 1) {
27
+ if (masked[index] === '(') depth += 1;
28
+ else if (masked[index] === ')') {
29
+ depth -= 1;
30
+ if (depth === 0) return index;
31
+ }
32
+ }
33
+ return masked.length;
34
+ }
35
+
36
+ /** Where each hand-rolled upgrade starts, as an offset into `masked`. */
37
+ function handRolledAt(masked: string): readonly number[] {
38
+ const at: number[] = [];
39
+ for (const match of masked.matchAll(SERVE)) {
40
+ const open = match.index + match[0].length - 1;
41
+ if (WEBSOCKET_KEY.test(masked.slice(open, closeOf(masked, open)))) at.push(match.index);
42
+ }
43
+ for (const pattern of [UPGRADE, HANDLERS]) {
44
+ for (const match of masked.matchAll(pattern)) at.push(match.index);
45
+ }
46
+ return at;
47
+ }
48
+
49
+ /** 1-based line of `offset`; `maskLiterals` keeps every newline, so the count is the source's. */
50
+ const lineOf = (masked: string, offset: number): number =>
51
+ masked.slice(0, offset).split('\n').length;
52
+
53
+ /** One `X_SOCKET_HAND_ROLLED` per app file that serves a socket itself, at its first line. */
54
+ export function handRolledSocketFindings(files: readonly SourceFile[]): readonly Finding[] {
55
+ return files.flatMap(({ path, source }): Finding[] => {
56
+ if (!source.includes('upgrade') && !source.includes('websocket')) return [];
57
+ const masked = maskLiterals(source);
58
+ const first = Math.min(...handRolledAt(masked));
59
+ if (!Number.isFinite(first)) return [];
60
+ const line = lineOf(masked, first);
61
+ return [
62
+ {
63
+ code: 'X_SOCKET_HAND_ROLLED',
64
+ cause: `${path}:${String(line)} upgrades a websocket itself, so the token check before the handshake, the per-actor cap, the heartbeat, the frame limit and the drain are the app's to remember — and the boot never serves it on the web listener`,
65
+ fix: `x verify --only boundaries --json # in ${commentSafe(path)}, delete the upgrade and declare the socket: export const link = deviceSocket({ path, resolveToken, onFrame }) from @ultimat3/realtime/server`,
66
+ docs: ERROR_DOCS_URL,
67
+ at: path,
68
+ meta: { line },
69
+ },
70
+ ];
71
+ });
72
+ }
@@ -0,0 +1,29 @@
1
+ // Every shape in an app's source whose literal is a catalog key. Here and not in `@ultimat3/i18n`:
2
+ // tier 1 must not name `blocks` or `defineMail`, and this package already sits over both. Passed by
3
+ // `scanSource` only, so the framework's own catalog scan (`scripts/lib/i18n-scan.ts`) keeps its
4
+ // callees: `mail.*` lives in `mail/src/catalog.ts`, not in `catalogs/en.json`.
5
+
6
+ import type { ExtractOptions } from '@ultimat3/i18n';
7
+
8
+ export const APP_KEY_SHAPES = {
9
+ callees: [
10
+ 't',
11
+ '$t',
12
+ 'translate',
13
+ // The key is the first argument of each (`mail/src/blocks.ts`). `divider` takes none.
14
+ // `i18n-app-key-shapes.test.ts` reads `blocks` itself, so a new block reds until listed here.
15
+ 'blocks.heading',
16
+ 'blocks.paragraph',
17
+ 'blocks.button',
18
+ 'blocks.callout',
19
+ 'blocks.detail',
20
+ ],
21
+ properties: [
22
+ // A push payload's and an admin screen's keys. Unambiguous anywhere: nothing else is `*Key`.
23
+ { name: 'titleKey' },
24
+ { name: 'bodyKey' },
25
+ // `subject:` alone is also a column, a form field and a schema member.
26
+ // The bare name, not `defineMail(`: `defineMail<{ total: string }>({` is the usual spelling.
27
+ { name: 'subject', whenFileContains: 'defineMail' },
28
+ ],
29
+ } as const satisfies ExtractOptions;
package/src/i18n-audit.ts CHANGED
@@ -19,13 +19,15 @@ import {
19
19
  missingFrom,
20
20
  nestCatalog,
21
21
  } from '@ultimat3/i18n';
22
+ import { APP_KEY_SHAPES } from './i18n-app-key-shapes';
22
23
  import { isLoudMiss, loudMiss } from './i18n-registration';
23
24
  import { posixRelative } from './posix-path';
24
25
  import { eachSourceFile, isTest } from './source-files';
25
26
  import { CATALOG_ROOT, catalogPath } from './templates/locales';
26
27
 
27
28
  /**
28
- * Every `t()` call the app's own source makes. `source-files.ts` (`eachSourceFile`) is the one
29
+ * Every key the app's own source names: a `t()` call, a mail block or subject, a push payload
30
+ * (`APP_KEY_SHAPES`). `source-files.ts` (`eachSourceFile`) is the one
29
31
  * glob set every text-scanning gate step already shares (`errors`, `filesize`) — a second glob
30
32
  * here would mean this command and `x verify` disagree on what "the app's source" is. Test files
31
33
  * are excluded: a fixture's `t('fixture.key')` is not a gap the shipped catalogs owe an answer to.
@@ -40,7 +42,10 @@ export async function scanSource(root: string): Promise<Extraction> {
40
42
  for await (const file of eachSourceFile(root)) {
41
43
  if (!isTest(file) && !isGuard(file)) files.push(file);
42
44
  }
43
- const extraction = await extractFromFiles(files.map((file) => join(root, file)));
45
+ const extraction = await extractFromFiles(
46
+ files.map((file) => join(root, file)),
47
+ APP_KEY_SHAPES,
48
+ );
44
49
  const toRelative = (file: string): string => posixRelative(root, file);
45
50
  return {
46
51
  usages: extraction.usages.map((usage) => ({ ...usage, file: toRelative(usage.file) })),
@@ -115,7 +120,9 @@ export async function auditApp(root: string): Promise<AuditFacts> {
115
120
  const [extraction, catalogs] = await Promise.all([scanSource(root), loadCatalogs(root)]);
116
121
  const ignoreUnused = runtimeKeyPatterns(extraction);
117
122
  return {
118
- report: auditCatalogs({ extraction, catalogs, ignoreUnused }),
123
+ // `reference` is what sees a key no static shape names: `t(\`plans.\${x}\`)` reads the same
124
+ // catalog in every locale, so a key `en` holds alone is a raw key on another locale's page.
125
+ report: auditCatalogs({ extraction, catalogs, ignoreUnused, reference: DEFAULT_LOCALE }),
119
126
  catalogs,
120
127
  extraction,
121
128
  ignoreUnused,
@@ -15,6 +15,7 @@ import {
15
15
  catalogRegistrationGaps,
16
16
  catalogsNeverRegistered,
17
17
  catalogUnregistered,
18
+ familyIncomplete,
18
19
  pluralVariantsOf,
19
20
  registeredLocales,
20
21
  } from '@ultimat3/i18n';
@@ -26,6 +27,7 @@ import type { DuplicateProbe } from './duplicate-packages';
26
27
  import { duplicateCause, duplicateFinding, findDuplicateInstalls } from './duplicate-packages';
27
28
  import { auditApp } from './i18n-audit';
28
29
  import { localeParityFindings } from './i18n-locale-parity';
30
+ import { mailPreviewFindings } from './mail-preview-findings';
29
31
  import type { Finding } from './output';
30
32
  import { findingFrom } from './output';
31
33
  import { CATALOG_ROOT, catalogPath } from './templates/locales';
@@ -105,7 +107,7 @@ export async function checkRegistration(input: RegistrationInput): Promise<Regis
105
107
  duplicate === undefined
106
108
  ? { ...finding, ...unregisteredFix(gap.locale, index), at: catalogPath(gap.locale) }
107
109
  : // The registry's own fix names a source edit — move the `defineCatalogs()` call — that an
108
- // agent following it performs on a file that is already right (ai-maxxing, 2026-09-05).
110
+ // agent following it performs on a file that is already right (one adopter, 2026-09-05).
109
111
  // With a duplicate on disk the cause IS the install, so the finding says so and the fix
110
112
  // is the one the duplicate carries.
111
113
  {
@@ -261,7 +263,9 @@ export function withPlaceholdersMissing(
261
263
  const locales = report.locales.map((audit) => {
262
264
  const catalog = catalogs[audit.locale] ?? {};
263
265
  const known = new Set(audit.missing);
264
- const placeheld = report.used.filter(
266
+ // `required`, not `used`: `x i18n sync` seeds a reference-only key as a marker too, and a marker
267
+ // that closed the finding would put `⟦…⟧` on the page under a green gate.
268
+ const placeheld = report.required.filter(
265
269
  (key) => !known.has(key) && answersWithPlaceholder(catalog, key),
266
270
  );
267
271
  if (placeheld.length === 0) return audit;
@@ -276,12 +280,21 @@ export function withPlaceholdersMissing(
276
280
  * the same finding, and a second construction of it is two renderers of one fact waiting to drift.
277
281
  */
278
282
  export function missingKeyFindings(report: ExtractReport): readonly Finding[] {
279
- return report.locales
283
+ const missing = report.locales
280
284
  .filter((audit) => audit.missing.length > 0)
281
285
  .map((audit) => ({
282
286
  ...findingFrom(catalogMissingKeys(audit.locale, audit.missing)),
283
287
  at: catalogPath(audit.locale),
284
288
  }));
289
+ // A select or ordinal family with no `_other` renders `⟦key⟧` for every value it does not name:
290
+ // the same loud miss, one family wide, so the same gate refuses it.
291
+ const incomplete = report.locales
292
+ .filter((audit) => audit.incomplete.length > 0)
293
+ .map((audit) => ({
294
+ ...findingFrom(familyIncomplete(audit.locale, audit.incomplete)),
295
+ at: catalogPath(audit.locale),
296
+ }));
297
+ return [...missing, ...incomplete];
285
298
  }
286
299
 
287
300
  /**
@@ -300,5 +313,7 @@ export async function catalogFindings(root: string): Promise<readonly Finding[]>
300
313
  ...missingKeyFindings(withPlaceholdersMissing(report, catalogs)),
301
314
  ...registration.findings,
302
315
  ...localeParityFindings(catalogs),
316
+ // The app is loaded by the audit above, so every `defineMail({ preview })` is registered.
317
+ ...mailPreviewFindings(Object.keys(catalogs)),
303
318
  ];
304
319
  }
package/src/index.ts CHANGED
@@ -113,6 +113,8 @@ export { renderJobTable } from './jobs-table';
113
113
  export { msg } from './messages';
114
114
  export { DEFAULT_METRICS_PORT, startMetricsEndpoint } from './metrics-endpoint';
115
115
  export { parseMigrationSql, readMigrations } from './migrations';
116
+ export type { NewShape } from './new-shapes';
117
+ export { NEW_SHAPES } from './new-shapes';
116
118
  export type { CommandResult, Finding, JsonValue } from './output';
117
119
  export { exitCodeFor, findingFrom, render, renderFinding, renderHuman } from './output';
118
120
  export type { CommandSpec, FlagSpec, ParsedArgs } from './parse';
@@ -36,7 +36,7 @@ import {
36
36
  realtimeIslandEntry,
37
37
  realtimeKind,
38
38
  } from './island-realtime';
39
- import type { IslandRealtime } from './island-realtime-state';
39
+ import { appPersistsRecords, type IslandRealtime } from './island-realtime-state';
40
40
  import type { Runtimes } from './island-runtime';
41
41
  import { loadingRuntime, mergeBuilds, runtimeBuilder, runtimeShared } from './island-runtime';
42
42
  import { solidDedupePlugin } from './island-solid-dedupe';
@@ -130,7 +130,7 @@ export async function discoverIslands(root: string): Promise<readonly string[]>
130
130
  /**
131
131
  * One `Bun.build` over `files`. With `splitting`, a module two of them import becomes one shared
132
132
  * chunk, fetched once by a page that renders both — a page rendering two islands that shared an
133
- * upload helper downloaded it twice without (notificado.co, 2026-09-29: 55.5 kB for ~34 kB of
133
+ * upload helper downloaded it twice without (one adopter, 2026-09-29: 55.5 kB for ~34 kB of
134
134
  * code). Without, `files` is one island and its chunk is self-contained. Either way a route's cost
135
135
  * is the graph walk `measureDocumentJs` does off the emitted files.
136
136
  */
@@ -144,9 +144,10 @@ async function buildAll(
144
144
  // Inside the refusal: the realtime probe PARSES the island's graph, and a file that will not
145
145
  // parse rejected with a raw `AggregateError: Failed to scan imports` — out of the web boot, with
146
146
  // no code, no file and no fix — before `Bun.build` ever ran to raise the one below.
147
+ const persists = appPersistsRecords();
147
148
  const realtime = await Promise.all(
148
149
  files.map((file) =>
149
- realtimeKind(root, file).catch((error: unknown) => {
150
+ realtimeKind(root, file, persists).catch((error: unknown) => {
150
151
  throw new IslandBuildFailedError({ file, logs: describeBuildError(error) });
151
152
  }),
152
153
  ),