@starci/hfs 4.2.17 → 4.4.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 (564) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/README.md +7 -0
  3. package/db/migrate.mjs +361 -0
  4. package/db/post-cutover.mjs +136 -0
  5. package/emit/db-types.mjs +10 -1
  6. package/lint/run.mjs +1 -1
  7. package/package.json +4 -1
  8. package/release/gate.mjs +298 -0
  9. package/release/index.mjs +103 -0
  10. package/release/push.mjs +110 -0
  11. package/runtime/knowledge/hfs/canon-pins.yaml +11 -11
  12. package/runtime/knowledge/hfs/fe-stack.yaml +22 -1
  13. package/runtime/knowledge/hfs/rules.yaml +3437 -160
  14. package/runtime/knowledge/hfs/slots.yaml +430 -92
  15. package/runtime/knowledge/patterns/be/api.yaml +92 -2
  16. package/runtime/knowledge/patterns/be/cli.yaml +37 -9
  17. package/runtime/knowledge/patterns/be/event-bus.yaml +3 -3
  18. package/runtime/knowledge/patterns/be/jobs.yaml +20 -2
  19. package/runtime/knowledge/patterns/be/projections.yaml +4 -2
  20. package/runtime/knowledge/patterns/be/queues.yaml +11 -3
  21. package/runtime/knowledge/patterns/be/reactors.yaml +20 -1
  22. package/runtime/knowledge/patterns/be/realtime.yaml +25 -3
  23. package/runtime/knowledge/patterns/be/saga.yaml +20 -3
  24. package/runtime/knowledge/patterns/be/webhooks.yaml +210 -36
  25. package/runtime/knowledge/patterns/fe/folder.yaml +4 -0
  26. package/runtime/knowledge/sonar-gate.yaml +1 -1
  27. package/runtime/modules/kernel/failure-codes.yaml +3128 -47
  28. package/runtime/modules/models/runtimes.yaml +1 -1
  29. package/runtime/modules/schemas/application-stacks.schema.yaml +663 -0
  30. package/runtime/packages/cli/src/catalog.generated.mjs +14104 -0
  31. package/runtime/packages/cli/src/validate-args.mjs +213 -0
  32. package/runtime/scripts/api/git/add.mjs +5 -0
  33. package/runtime/scripts/api/git/cat-file.mjs +5 -0
  34. package/runtime/scripts/api/git/init.mjs +7 -0
  35. package/runtime/scripts/api/git/lib.mjs +3 -0
  36. package/runtime/scripts/api/git/tag.mjs +5 -0
  37. package/runtime/scripts/api/node/lib.mjs +20 -0
  38. package/runtime/scripts/api/node/run-node.mjs +6 -0
  39. package/runtime/scripts/api/sops/lib.mjs +41 -0
  40. package/runtime/scripts/api/sops/resolve-sops.mjs +7 -0
  41. package/runtime/scripts/api/sops/seal.mjs +1 -1
  42. package/runtime/scripts/api/sops/selected-age-recipient.mjs +7 -0
  43. package/runtime/scripts/api/supabase/database-dump.mjs +5 -0
  44. package/runtime/scripts/api/supabase/database-query.mjs +5 -0
  45. package/runtime/scripts/api/supabase/lib.mjs +44 -0
  46. package/runtime/scripts/api/supabase/migration-repair.mjs +5 -0
  47. package/runtime/scripts/hfs/app-wiring.mjs +201 -0
  48. package/runtime/scripts/hfs/architecture/backend-app-files.mjs +103 -0
  49. package/runtime/scripts/hfs/architecture/backend.mjs +22 -10
  50. package/runtime/scripts/hfs/architecture/background-unowned.mjs +157 -41
  51. package/runtime/scripts/hfs/architecture/canon-clones.mjs +240 -0
  52. package/runtime/scripts/hfs/architecture/canon-jsx-clones.mjs +143 -0
  53. package/runtime/scripts/hfs/architecture/client-reaches-server.mjs +6 -3
  54. package/runtime/scripts/hfs/architecture/clones.mjs +17 -8
  55. package/runtime/scripts/hfs/architecture/connection-map.mjs +14 -12
  56. package/runtime/scripts/hfs/architecture/context-map.mjs +25 -14
  57. package/runtime/scripts/hfs/architecture/context-owner.mjs +1 -2
  58. package/runtime/scripts/hfs/architecture/dead-exports-bindings.mjs +149 -0
  59. package/runtime/scripts/hfs/architecture/dead-exports.mjs +23 -50
  60. package/runtime/scripts/hfs/architecture/default-deny.mjs +8 -6
  61. package/runtime/scripts/hfs/architecture/frontend-block-render.mjs +104 -0
  62. package/runtime/scripts/hfs/architecture/frontend-world-render.mjs +30 -11
  63. package/runtime/scripts/hfs/architecture/frontend-world.mjs +1 -1
  64. package/runtime/scripts/hfs/architecture/function-spec-providers.mjs +153 -0
  65. package/runtime/scripts/hfs/architecture/hfs.mjs +3 -3
  66. package/runtime/scripts/hfs/architecture/i18n-keys.mjs +16 -3
  67. package/runtime/scripts/hfs/architecture/machine-ast.mjs +17 -0
  68. package/runtime/scripts/hfs/architecture/module-per-transport.mjs +49 -25
  69. package/runtime/scripts/hfs/architecture/next-data-inference.mjs +176 -0
  70. package/runtime/scripts/hfs/architecture/next-data-symbols.mjs +26 -0
  71. package/runtime/scripts/hfs/architecture/next-data.mjs +31 -23
  72. package/runtime/scripts/hfs/architecture/reachability.mjs +14 -0
  73. package/runtime/scripts/hfs/architecture/register-once.mjs +15 -7
  74. package/runtime/scripts/hfs/architecture/required-files.mjs +2 -1
  75. package/runtime/scripts/hfs/architecture/source-names.mjs +48 -3
  76. package/runtime/scripts/hfs/architecture/staged-consumers.mjs +37 -0
  77. package/runtime/scripts/hfs/architecture/supabase-ast.mjs +214 -17
  78. package/runtime/scripts/hfs/architecture/supabase-be.mjs +1 -6
  79. package/runtime/scripts/hfs/architecture/supabase.mjs +59 -21
  80. package/runtime/scripts/hfs/architecture/surface.mjs +2 -1
  81. package/runtime/scripts/hfs/architecture/tiers.mjs +11 -4
  82. package/runtime/scripts/hfs/architecture/typescript.mjs +1 -8
  83. package/runtime/scripts/hfs/architecture/unit-spec-providers.mjs +9 -13
  84. package/runtime/scripts/hfs/check-results.mjs +28 -0
  85. package/runtime/scripts/hfs/check.mjs +63 -72
  86. package/runtime/scripts/hfs/coverage-from.mjs +70 -0
  87. package/runtime/scripts/hfs/db-baseline-verb.mjs +276 -0
  88. package/runtime/scripts/hfs/db-plan-verb.mjs +254 -0
  89. package/runtime/scripts/hfs/db-restamp-verb.mjs +60 -0
  90. package/runtime/scripts/hfs/declaration-shape.mjs +39 -3
  91. package/runtime/scripts/hfs/doc-commands.mjs +367 -0
  92. package/runtime/scripts/hfs/edition-slots.mjs +2 -2
  93. package/runtime/scripts/hfs/example-secrets.mjs +158 -0
  94. package/runtime/scripts/hfs/manifest-shape.mjs +91 -6
  95. package/runtime/scripts/hfs/path-findings.mjs +32 -2
  96. package/runtime/scripts/hfs/refusal-codes.mjs +6 -0
  97. package/runtime/scripts/hfs/rule-catalog.mjs +156 -11
  98. package/runtime/scripts/hfs/rule-params-shape.mjs +22 -10
  99. package/runtime/scripts/hfs/rules/a15-findings.mjs +88 -0
  100. package/runtime/scripts/hfs/rules/app-env-required.mjs +20 -0
  101. package/runtime/scripts/hfs/rules/app-root.mjs +2 -2
  102. package/runtime/scripts/hfs/rules/assets.mjs +79 -0
  103. package/runtime/scripts/hfs/rules/boot-proof.mjs +16 -0
  104. package/runtime/scripts/hfs/rules/canon-copy-drift.mjs +59 -0
  105. package/runtime/scripts/hfs/rules/contract.mjs +66 -5
  106. package/runtime/scripts/hfs/rules/coverage-below.mjs +230 -0
  107. package/runtime/scripts/hfs/rules/database-access.mjs +343 -0
  108. package/runtime/scripts/hfs/rules/database-baseline.mjs +142 -0
  109. package/runtime/scripts/hfs/rules/database-config.mjs +250 -2
  110. package/runtime/scripts/hfs/rules/database-constants.mjs +9 -0
  111. package/runtime/scripts/hfs/rules/database-migrations.mjs +317 -37
  112. package/runtime/scripts/hfs/rules/database-plpgsql.mjs +119 -9
  113. package/runtime/scripts/hfs/rules/database-queue-sources.mjs +176 -0
  114. package/runtime/scripts/hfs/rules/database-queues.mjs +361 -0
  115. package/runtime/scripts/hfs/rules/database-sql.mjs +91 -9
  116. package/runtime/scripts/hfs/rules/database.mjs +11 -2
  117. package/runtime/scripts/hfs/rules/deploy-scan.mjs +192 -0
  118. package/runtime/scripts/hfs/rules/deploy-workflow.mjs +219 -0
  119. package/runtime/scripts/hfs/rules/doc-fresh.mjs +286 -0
  120. package/runtime/scripts/hfs/rules/docker-health.mjs +249 -0
  121. package/runtime/scripts/hfs/rules/docker.mjs +24 -7
  122. package/runtime/scripts/hfs/rules/edition.mjs +11 -10
  123. package/runtime/scripts/hfs/rules/env-parity.mjs +373 -0
  124. package/runtime/scripts/hfs/rules/env-value-shape.mjs +113 -0
  125. package/runtime/scripts/hfs/rules/event-bus.mjs +3 -2
  126. package/runtime/scripts/hfs/rules/event-tables.mjs +100 -0
  127. package/runtime/scripts/hfs/rules/fe-contract-documents.mjs +115 -15
  128. package/runtime/scripts/hfs/rules/fe-stack.mjs +57 -0
  129. package/runtime/scripts/hfs/rules/feature-files.mjs +28 -0
  130. package/runtime/scripts/hfs/rules/frontend-tree.mjs +54 -35
  131. package/runtime/scripts/hfs/rules/integration-specs.mjs +3 -2
  132. package/runtime/scripts/hfs/rules/lexicon.mjs +62 -0
  133. package/runtime/scripts/hfs/rules/lite-proof-lane.mjs +89 -0
  134. package/runtime/scripts/hfs/rules/monorepo.mjs +14 -1
  135. package/runtime/scripts/hfs/rules/pipeline.mjs +8 -1
  136. package/runtime/scripts/hfs/rules/proof-commands.mjs +2 -0
  137. package/runtime/scripts/hfs/rules/release-provenance.mjs +211 -0
  138. package/runtime/scripts/hfs/rules/saga-delivery.mjs +66 -0
  139. package/runtime/scripts/hfs/rules/saga.mjs +8 -16
  140. package/runtime/scripts/hfs/rules/script-hygiene.mjs +151 -0
  141. package/runtime/scripts/hfs/rules/secret-custody-only.mjs +65 -0
  142. package/runtime/scripts/hfs/rules/secrets.mjs +26 -0
  143. package/runtime/scripts/hfs/rules/services.mjs +4 -4
  144. package/runtime/scripts/hfs/rules/stack-tooling.mjs +93 -0
  145. package/runtime/scripts/hfs/rules/stacks-deploy.mjs +76 -0
  146. package/runtime/scripts/hfs/rules/stacks-ingress.mjs +87 -0
  147. package/runtime/scripts/hfs/rules/stacks-lite-backing.mjs +111 -0
  148. package/runtime/scripts/hfs/rules/stacks-release.mjs +366 -0
  149. package/runtime/scripts/hfs/rules/stacks-runtime.mjs +168 -0
  150. package/runtime/scripts/hfs/rules/stacks-swarm.mjs +417 -0
  151. package/runtime/scripts/hfs/rules/stacks.mjs +13 -8
  152. package/runtime/scripts/hfs/rules/supabase-secrets.mjs +1 -1
  153. package/runtime/scripts/hfs/rules/supply-chain.mjs +167 -0
  154. package/runtime/scripts/hfs/rules/unused-symbol-flow.mjs +101 -0
  155. package/runtime/scripts/hfs/rules/unused-symbol-ledger.mjs +77 -0
  156. package/runtime/scripts/hfs/rules/unused-symbol-translators.mjs +165 -0
  157. package/runtime/scripts/hfs/rules/unused-symbol-values.mjs +103 -0
  158. package/runtime/scripts/hfs/rules/unused-symbol.mjs +320 -0
  159. package/runtime/scripts/hfs/runtime-rules/source-ast.mjs +185 -0
  160. package/runtime/scripts/hfs/runtime-rules/workflow-source.mjs +19 -0
  161. package/runtime/scripts/hfs/sealed-members.mjs +101 -0
  162. package/runtime/scripts/hfs/secret-bootstrap-crypto.mjs +200 -0
  163. package/runtime/scripts/hfs/secret-document.mjs +97 -0
  164. package/runtime/scripts/hfs/secret-edit.mjs +170 -0
  165. package/runtime/scripts/hfs/secret-errors.mjs +20 -0
  166. package/runtime/scripts/hfs/secret-files.mjs +124 -0
  167. package/runtime/scripts/hfs/secret-init.mjs +300 -0
  168. package/runtime/scripts/hfs/secret-output.mjs +100 -0
  169. package/runtime/scripts/hfs/secret-sops.mjs +130 -0
  170. package/runtime/scripts/hfs/secret-store.mjs +163 -0
  171. package/runtime/scripts/hfs/secret.mjs +162 -193
  172. package/runtime/scripts/hfs/slot-imports.mjs +18 -3
  173. package/runtime/scripts/hfs/slot-required.mjs +13 -0
  174. package/runtime/scripts/hfs/slot-semantic-problems.mjs +2 -1
  175. package/runtime/scripts/hfs/slots.mjs +1 -0
  176. package/runtime/scripts/hfs/stack-config-kit.mjs +34 -0
  177. package/runtime/scripts/hfs/stack-deploy-config.mjs +313 -0
  178. package/runtime/scripts/hfs/stack-ingress-config.mjs +237 -0
  179. package/runtime/scripts/hfs/stacks-gate.mjs +414 -0
  180. package/runtime/scripts/hfs/starcistacks-runtime.mjs +61 -0
  181. package/runtime/scripts/hfs/tree-findings.mjs +22 -0
  182. package/runtime/scripts/lib/command-tokens.mjs +126 -0
  183. package/runtime/scripts/lib/compose-values.mjs +11 -0
  184. package/runtime/scripts/lib/deployment-findings.mjs +7 -0
  185. package/runtime/scripts/lib/json-clone.mjs +61 -0
  186. package/runtime/scripts/lib/list.mjs +4 -0
  187. package/runtime/scripts/lib/roots.mjs +59 -0
  188. package/runtime/scripts/lib/runtime-example.mjs +13 -0
  189. package/runtime/scripts/lib/secret-patterns.mjs +60 -5
  190. package/runtime/scripts/lib/sql-ast.mjs +32 -0
  191. package/runtime/scripts/lib/stack-services.mjs +1 -1
  192. package/runtime/scripts/lib/swarm-stack.mjs +126 -0
  193. package/runtime/scripts/lib/trim-end.mjs +8 -0
  194. package/runtime/scripts/lib/ts-analysis.mjs +23 -0
  195. package/runtime/scripts/lib/ts-ast.mjs +27 -2
  196. package/runtime/scripts/lib/workflow-events.mjs +8 -0
  197. package/scaffold/add-app.mjs +77 -59
  198. package/scaffold/add-cli-lite.mjs +24 -17
  199. package/scaffold/add-compose.mjs +79 -0
  200. package/scaffold/add-database-lite.mjs +14 -0
  201. package/scaffold/add-fe-overlay.mjs +122 -0
  202. package/scaffold/add-fe-page.mjs +269 -0
  203. package/scaffold/add-inbox-lite.mjs +87 -124
  204. package/scaffold/add-plan.mjs +183 -0
  205. package/scaffold/add-table.mjs +132 -70
  206. package/scaffold/add.mjs +186 -204
  207. package/scaffold/app.mjs +128 -43
  208. package/scaffold/edition-gate.mjs +3 -3
  209. package/scaffold/image.mjs +2 -2
  210. package/scaffold/lite-exports.mjs +2 -1
  211. package/scaffold/service.mjs +2 -2
  212. package/src/main.mjs +238 -119
  213. package/stack/age-install.mjs +37 -0
  214. package/stack/backup.mjs +100 -0
  215. package/stack/build-args.mjs +42 -0
  216. package/stack/build.mjs +173 -0
  217. package/stack/cli.mjs +130 -0
  218. package/stack/config.mjs +57 -0
  219. package/stack/context.mjs +57 -0
  220. package/stack/converge.mjs +107 -0
  221. package/stack/deploy.mjs +301 -0
  222. package/stack/digest.mjs +16 -0
  223. package/stack/docker-transport.mjs +53 -0
  224. package/stack/edge-alias.mjs +118 -0
  225. package/stack/errors.mjs +21 -0
  226. package/stack/image-scan.mjs +152 -0
  227. package/stack/ingress-create.mjs +114 -0
  228. package/stack/ingress-publish.mjs +273 -0
  229. package/stack/ingress.mjs +706 -0
  230. package/stack/jobs.mjs +51 -0
  231. package/stack/live-restore.mjs +177 -0
  232. package/stack/prepare.mjs +266 -0
  233. package/stack/private-file.mjs +22 -0
  234. package/stack/provision.mjs +90 -0
  235. package/stack/prune.mjs +46 -0
  236. package/stack/release-store.mjs +87 -0
  237. package/stack/remote.mjs +205 -0
  238. package/stack/render.mjs +149 -0
  239. package/stack/retire.mjs +47 -0
  240. package/stack/sealed.mjs +149 -0
  241. package/stack/task.mjs +42 -0
  242. package/stack/verify.mjs +99 -0
  243. package/sync/ci-commands.mjs +13 -0
  244. package/sync/index.mjs +127 -64
  245. package/sync/stack-images.mjs +127 -0
  246. package/templates/app/ci-workflows/github/workflows/ci.yml +5 -1
  247. package/templates/app/ci-workflows/github/workflows/images.yml +15 -3
  248. package/templates/app/ci-workflows/github/workflows/stack-deploy.yml +148 -0
  249. package/templates/app/ci-workflows-lite/github/workflows/ci.yml +7 -3
  250. package/templates/app/ci-workflows-lite/github/workflows/db-deploy.yml +38 -6
  251. package/templates/app/ci-workflows-lite/github/workflows/images.yml +14 -2
  252. package/templates/app/ci-workflows-lite/github/workflows/stack-deploy.yml +148 -0
  253. package/templates/app/codegen-graphql.mjs.partial +13 -2
  254. package/templates/app/codegen-lite/scripts/codegen.mjs +6 -1
  255. package/templates/app/codegen-prelude.mjs.partial +8 -0
  256. package/templates/app/gitignore +4 -0
  257. package/templates/app/gitignore-lite +4 -0
  258. package/templates/app/hooks/husky/pre-commit +7 -12
  259. package/templates/app/package-scripts/package.json +1 -0
  260. package/templates/app/package-scripts/supabase.json +7 -0
  261. package/templates/app/package-scripts-lite/package.json +1 -7
  262. package/templates/app/skeleton/.editorconfig +1 -0
  263. package/templates/app/skeleton/.gitattributes +7 -1
  264. package/templates/app/skeleton/.github/dependabot.yml +17 -0
  265. package/templates/app/skeleton/.nvmrc +1 -1
  266. package/templates/app/skeleton/.sops.yaml +1 -1
  267. package/templates/app/skeleton/.starcistacks/application-stacks.yaml +134 -1
  268. package/templates/app/skeleton/.starcistacks/dev/README.md +16 -0
  269. package/templates/app/skeleton/.starcistacks/dev/environment.json +5 -0
  270. package/templates/app/skeleton/.starcistacks/dev/infra/compose/stack.yaml +71 -0
  271. package/templates/app/skeleton/.starcistacks/dev/runtime/env/KEYS.md +14 -0
  272. package/templates/app/skeleton/.starcistacks/production/README.md +37 -0
  273. package/templates/app/skeleton/.starcistacks/production/environment.json +5 -0
  274. package/templates/app/skeleton/.starcistacks/production/infra/swarm/Caddyfile +15 -0
  275. package/templates/app/skeleton/.starcistacks/production/infra/swarm/owner-actions.md +11 -0
  276. package/templates/app/skeleton/.starcistacks/production/infra/swarm/recovery/restore-proof.md +7 -0
  277. package/templates/app/skeleton/.starcistacks/production/infra/swarm/releases/current.yaml +6 -0
  278. package/templates/app/skeleton/.starcistacks/production/infra/swarm/releases/images.lock.yaml +9 -0
  279. package/templates/app/skeleton/.starcistacks/production/infra/swarm/releases/previous.yaml +6 -0
  280. package/templates/app/skeleton/.starcistacks/production/infra/swarm/stack.yaml +142 -0
  281. package/templates/app/skeleton/.starcistacks/production/runtime/env/KEYS.md +14 -0
  282. package/templates/app/skeleton/README.md +6 -6
  283. package/templates/app/skeleton/scripts/codegen.mjs +1 -2
  284. package/templates/app/skeleton-lite/.editorconfig +16 -0
  285. package/templates/app/skeleton-lite/.gitattributes +8 -0
  286. package/templates/app/skeleton-lite/.github/dependabot.yml +17 -0
  287. package/templates/app/skeleton-lite/.nvmrc +1 -0
  288. package/templates/app/skeleton-lite/.sops.yaml +3 -2
  289. package/templates/app/skeleton-lite/.starcistacks/application-stacks.yaml +193 -0
  290. package/templates/app/skeleton-lite/.starcistacks/dev/README.md +20 -0
  291. package/templates/app/skeleton-lite/.starcistacks/dev/environment.json +2 -1
  292. package/templates/app/skeleton-lite/.starcistacks/dev/infra/swarm/Caddyfile +16 -0
  293. package/templates/app/skeleton-lite/.starcistacks/dev/infra/swarm/owner-actions.md +11 -0
  294. package/templates/app/skeleton-lite/.starcistacks/dev/infra/swarm/recovery/restore-proof.md +7 -0
  295. package/templates/app/skeleton-lite/.starcistacks/dev/infra/swarm/releases/current.yaml +6 -0
  296. package/templates/app/skeleton-lite/.starcistacks/dev/infra/swarm/releases/images.lock.yaml +8 -0
  297. package/templates/app/skeleton-lite/.starcistacks/dev/infra/swarm/releases/previous.yaml +6 -0
  298. package/templates/app/skeleton-lite/.starcistacks/dev/infra/swarm/stack.yaml +120 -0
  299. package/templates/app/skeleton-lite/.starcistacks/dev/runtime/env/KEYS.md +18 -0
  300. package/templates/app/skeleton-lite/.starcistacks/production/README.md +38 -0
  301. package/templates/app/skeleton-lite/.starcistacks/production/environment.json +6 -0
  302. package/templates/app/skeleton-lite/.starcistacks/production/infra/swarm/Caddyfile +11 -0
  303. package/templates/app/skeleton-lite/.starcistacks/production/infra/swarm/owner-actions.md +11 -0
  304. package/templates/app/skeleton-lite/.starcistacks/production/infra/swarm/recovery/restore-proof.md +7 -0
  305. package/templates/app/skeleton-lite/.starcistacks/production/infra/swarm/releases/current.yaml +6 -0
  306. package/templates/app/skeleton-lite/.starcistacks/production/infra/swarm/releases/images.lock.yaml +8 -0
  307. package/templates/app/skeleton-lite/.starcistacks/production/infra/swarm/releases/previous.yaml +6 -0
  308. package/templates/app/skeleton-lite/.starcistacks/production/infra/swarm/stack.yaml +121 -0
  309. package/templates/app/skeleton-lite/.starcistacks/production/runtime/env/KEYS.md +21 -0
  310. package/templates/app/skeleton-lite/.starciwork/brand/index.yaml +12 -0
  311. package/templates/app/skeleton-lite/.starciwork/workspace.yaml +9 -0
  312. package/templates/app/skeleton-lite/README.md +8 -4
  313. package/templates/app/skeleton-lite/supabase/config.toml +8 -1
  314. package/templates/app/skeleton-lite/supabase/migrations/__timestamp___baseline.sql +15 -6
  315. package/templates/app/starciwork.gitignore-lite +6 -0
  316. package/templates/app/upgrade-full/.starcistacks/dev/README.md +13 -7
  317. package/templates/be/image/worker/Dockerfile +1 -1
  318. package/templates/be/inbox/database-client.spec.ts.tpl +22 -28
  319. package/templates/be/inbox/database-client.ts.tpl +14 -28
  320. package/templates/be/inbox/delivery-recorded.event.ts.tpl +36 -0
  321. package/templates/be/inbox/event-index.ts.tpl +1 -0
  322. package/templates/be/inbox/events.json.tpl +10 -0
  323. package/templates/be/inbox/migration.sql.tpl +1 -0
  324. package/templates/be/inbox/platform/event-bus/event-bus.contracts.ts.tpl +42 -0
  325. package/templates/be/inbox/platform/event-bus/event-bus.decorators.ts.tpl +9 -0
  326. package/templates/be/inbox/platform/event-bus/event-bus.module-definition.ts.tpl +6 -0
  327. package/templates/be/inbox/platform/event-bus/event-bus.module.ts.tpl +19 -0
  328. package/templates/be/inbox/platform/event-bus/event-bus.options.ts.tpl +2 -0
  329. package/templates/be/inbox/platform/event-bus/event-bus.port.ts.tpl +16 -0
  330. package/templates/be/inbox/platform/event-bus/event-bus.service.spec.ts.tpl +45 -0
  331. package/templates/be/inbox/platform/event-bus/event-bus.service.ts.tpl +15 -0
  332. package/templates/be/inbox/platform/event-bus/index.ts.tpl +5 -0
  333. package/templates/be/inbox/platform/event-bus/persistence/event-bus.sql.ts.tpl +5 -0
  334. package/templates/be/inbox/platform/inbox/inbox.decorators.ts.tpl +9 -0
  335. package/templates/be/inbox/platform/inbox/inbox.module-definition.ts.tpl +6 -0
  336. package/templates/be/inbox/platform/inbox/inbox.module.ts.tpl +19 -0
  337. package/templates/be/inbox/platform/inbox/inbox.options.ts.tpl +2 -0
  338. package/templates/be/inbox/platform/inbox/inbox.port.ts.tpl +10 -0
  339. package/templates/be/inbox/platform/inbox/inbox.service.spec.ts.tpl +41 -0
  340. package/templates/be/inbox/platform/inbox/inbox.service.ts.tpl +19 -0
  341. package/templates/be/inbox/platform/inbox/index.ts.tpl +3 -0
  342. package/templates/be/inbox/platform/inbox/persistence/inbox.sql.ts.tpl +7 -0
  343. package/templates/be/inbox/platform/migration.sql.tpl +48 -0
  344. package/templates/be/inbox/service.spec.ts.tpl +49 -20
  345. package/templates/be/inbox/service.ts.tpl +27 -16
  346. package/templates/be/inbox/sql.ts.tpl +2 -2
  347. package/templates/be/inbox/webhook-door.spec.ts.tpl +3 -3
  348. package/templates/be/inbox/webhook-door.ts.tpl +4 -3
  349. package/templates/be/patterns/api/feature.module-definition.ts.tpl +6 -0
  350. package/templates/be/patterns/api/feature.module.ts.tpl +2 -1
  351. package/templates/be/patterns/api/feature.options.ts.tpl +2 -0
  352. package/templates/be/patterns/api/graphql.module-definition.ts.tpl +6 -0
  353. package/templates/be/patterns/api/graphql.module.ts.tpl +3 -2
  354. package/templates/be/patterns/api/graphql.options.ts.tpl +2 -0
  355. package/templates/be/patterns/cli/group.module-definition.ts.tpl +6 -0
  356. package/templates/be/patterns/cli/group.module.ts.tpl +2 -1
  357. package/templates/be/patterns/cli/group.options.ts.tpl +2 -0
  358. package/templates/be/patterns/event-bus/platform/errors/event-bus.error.ts.tpl +0 -3
  359. package/templates/be/patterns/event-bus/platform/messages/event-bus.messages.ts.tpl +0 -2
  360. package/templates/be/patterns/jobs/job.module-definition.ts.tpl +6 -0
  361. package/templates/be/patterns/jobs/job.module.ts.tpl +2 -1
  362. package/templates/be/patterns/jobs/job.options.ts.tpl +2 -0
  363. package/templates/be/patterns/jobs/job.queue.module-definition.ts.tpl +6 -0
  364. package/templates/be/patterns/jobs/job.queue.module.ts.tpl +6 -3
  365. package/templates/be/patterns/jobs/job.queue.options.ts.tpl +2 -0
  366. package/templates/be/patterns/jobs/platform/fenced.processor.spec.ts.tpl +3 -1
  367. package/templates/be/patterns/outbox/platform/outbox-relay.policy.spec.ts.tpl +25 -9
  368. package/templates/be/patterns/queues/platform/queue.config.ts.tpl +1 -1
  369. package/templates/be/patterns/queues/platform/queue.index.ts.tpl +1 -1
  370. package/templates/be/patterns/queues/platform/queue.module-definition.ts.tpl +2 -2
  371. package/templates/be/patterns/queues/platform/queue.module.ts.tpl +11 -0
  372. package/templates/be/patterns/queues/platform/queue.options.ts.tpl +10 -0
  373. package/templates/be/patterns/queues/queue.module-definition.ts.tpl +6 -0
  374. package/templates/be/patterns/queues/queue.module.ts.tpl +2 -1
  375. package/templates/be/patterns/queues/queue.options.ts.tpl +2 -0
  376. package/templates/be/patterns/reactors/message.module-definition.ts.tpl +6 -0
  377. package/templates/be/patterns/reactors/message.module.ts.tpl +6 -3
  378. package/templates/be/patterns/reactors/message.options.ts.tpl +2 -0
  379. package/templates/be/patterns/reactors/reactor.module-definition.ts.tpl +6 -0
  380. package/templates/be/patterns/reactors/reactor.module.ts.tpl +2 -1
  381. package/templates/be/patterns/reactors/reactor.options.ts.tpl +2 -0
  382. package/templates/be/patterns/realtime/channel.gateway.spec.ts.tpl +188 -0
  383. package/templates/be/patterns/realtime/channel.gateway.ts.tpl +22 -0
  384. package/templates/be/patterns/realtime/channel.graphql.module-definition.ts.tpl +6 -0
  385. package/templates/be/patterns/realtime/channel.graphql.module.ts.tpl +2 -1
  386. package/templates/be/patterns/realtime/channel.graphql.options.ts.tpl +2 -0
  387. package/templates/be/patterns/realtime/channel.websocket.module-definition.ts.tpl +6 -0
  388. package/templates/be/patterns/realtime/channel.websocket.module.ts.tpl +110 -0
  389. package/templates/be/patterns/realtime/channel.websocket.options.ts.tpl +7 -0
  390. package/templates/be/patterns/saga/saga.message.module-definition.ts.tpl +6 -0
  391. package/templates/be/patterns/saga/saga.message.module.ts.tpl +6 -3
  392. package/templates/be/patterns/saga/saga.message.options.ts.tpl +2 -0
  393. package/templates/be/patterns/saga/saga.module-definition.ts.tpl +6 -0
  394. package/templates/be/patterns/saga/saga.module.ts.tpl +2 -1
  395. package/templates/be/patterns/saga/saga.options.ts.tpl +2 -0
  396. package/templates/be/patterns/webhooks/webhook.door.spec.ts.tpl +3 -3
  397. package/templates/be/patterns/webhooks/webhook.door.ts.tpl +3 -3
  398. package/templates/be/patterns/webhooks/webhook.http.module-definition.ts.tpl +6 -0
  399. package/templates/be/patterns/webhooks/webhook.http.module.ts.tpl +2 -1
  400. package/templates/be/patterns/webhooks/webhook.http.options.ts.tpl +2 -0
  401. package/templates/be/platform/graphql/localize-errors.mapper.spec.ts +80 -26
  402. package/templates/be/platform/graphql/localize-errors.mapper.ts +2 -5
  403. package/templates/be/skeleton/apps/__app__/src/__app__.env.ts +21 -0
  404. package/templates/be/skeleton/apps/__app__/src/__app__.options.ts +1 -1
  405. package/templates/be/skeleton/apps/__app__/src/app.module.ts +15 -45
  406. package/templates/be/skeleton/apps/__app__/src/compose/api.ts +4 -0
  407. package/templates/be/skeleton/apps/__app__/src/compose/domain.ts +11 -0
  408. package/templates/be/skeleton/apps/__app__/src/compose/platform.ts +44 -0
  409. package/templates/be/skeleton/apps/__app__/src/compose/system.ts +5 -0
  410. package/templates/be/skeleton/apps/__app__/src/compose/webhooks.ts +4 -0
  411. package/templates/be/skeleton/apps/__app__/src/main.ts +18 -10
  412. package/templates/be/skeleton/apps/cli/src/app.module.ts +3 -13
  413. package/templates/be/skeleton/apps/cli/src/cli.env.ts +13 -0
  414. package/templates/be/skeleton/apps/cli/src/cli.options.ts +7 -17
  415. package/templates/be/skeleton/apps/cli/src/compose/cli.ts +5 -0
  416. package/templates/be/skeleton/apps/cli/src/compose/platform.ts +24 -0
  417. package/templates/be/skeleton/apps/cli/src/main.ts +3 -2
  418. package/templates/be/skeleton/src/features/api/system-health/application/check-readiness.contracts.ts +2 -0
  419. package/templates/be/skeleton/src/features/api/system-health/application/check-readiness.handler.ts +22 -0
  420. package/templates/be/skeleton/src/features/api/system-health/application/check-readiness.query.ts +11 -0
  421. package/templates/be/skeleton/src/features/api/system-health/system-health.module-definition.ts +6 -0
  422. package/templates/be/skeleton/src/features/api/system-health/system-health.module.ts +5 -3
  423. package/templates/be/skeleton/src/features/api/system-health/system-health.options.ts +2 -0
  424. package/templates/be/skeleton/src/features/api/system-health/transport/http/ready.controller.ts +19 -0
  425. package/templates/be/skeleton/src/features/api/system-health/transport/http/system-health-http.module-definition.ts +6 -0
  426. package/templates/be/skeleton/src/features/api/system-health/transport/http/system-health-http.module.ts +8 -3
  427. package/templates/be/skeleton/src/features/api/system-health/transport/http/system-health-http.options.ts +2 -0
  428. package/templates/be/skeleton/src/features/cli/cli.module-definition.ts +6 -0
  429. package/templates/be/skeleton/src/features/cli/cli.module.ts +3 -2
  430. package/templates/be/skeleton/src/features/cli/cli.options.ts +2 -0
  431. package/templates/be/skeleton/src/features/cli/migrate/migrate.module-definition.ts +6 -0
  432. package/templates/be/skeleton/src/features/cli/migrate/migrate.module.ts +2 -1
  433. package/templates/be/skeleton/src/features/cli/migrate/migrate.options.ts +2 -0
  434. package/templates/be/skeleton/src/features/cli/seed/seed.module-definition.ts +6 -0
  435. package/templates/be/skeleton/src/features/cli/seed/seed.module.ts +2 -1
  436. package/templates/be/skeleton/src/features/cli/seed/seed.options.ts +2 -0
  437. package/templates/be/skeleton/src/features/cli/seed/subs/run.cli.spec.ts +5 -6
  438. package/templates/be/skeleton/src/modules/domain/identity/admission.policy.spec.ts +56 -4
  439. package/templates/be/skeleton/src/modules/domain/identity/admission.policy.ts +36 -5
  440. package/templates/be/skeleton/src/modules/domain/identity/auth.guard.spec.ts +39 -8
  441. package/templates/be/skeleton/src/modules/domain/identity/auth.guard.ts +14 -8
  442. package/templates/be/skeleton/src/modules/domain/identity/credential.contracts.ts +26 -0
  443. package/templates/be/skeleton/src/modules/domain/identity/credential.guard.spec.ts +119 -0
  444. package/templates/be/skeleton/src/modules/domain/identity/credential.guard.ts +43 -0
  445. package/templates/be/skeleton/src/modules/domain/identity/identity.contracts.ts +16 -0
  446. package/templates/be/skeleton/src/modules/domain/identity/identity.decorators.ts +18 -1
  447. package/templates/be/skeleton/src/modules/domain/identity/identity.module.ts +22 -4
  448. package/templates/be/skeleton/src/modules/domain/identity/identity.options.ts +8 -2
  449. package/templates/be/skeleton/src/modules/domain/identity/index.ts +1 -0
  450. package/templates/be/skeleton/src/modules/platform/config/env-source.config.ts +6 -6
  451. package/templates/be/skeleton/src/modules/platform/database/connection-source.client.spec.ts +11 -4
  452. package/templates/be/skeleton/src/modules/platform/database/database-probe.service.spec.ts +32 -0
  453. package/templates/be/skeleton/src/modules/platform/database/database-probe.service.ts +14 -0
  454. package/templates/be/skeleton/src/modules/platform/database/database.module.ts +7 -1
  455. package/templates/be/skeleton/src/modules/platform/database/index.ts +2 -0
  456. package/templates/be/skeleton/src/modules/platform/database/primary.decorators.ts +8 -0
  457. package/templates/be/skeleton/src/modules/platform/database/seed-connections.client.spec.ts +25 -6
  458. package/templates/be/skeleton/src/modules/platform/errors/errors.filter.spec.ts +16 -6
  459. package/templates/be/skeleton/src/modules/platform/http-security/edge-security.service.spec.ts +130 -0
  460. package/templates/be/skeleton/src/modules/platform/http-security/edge-security.service.ts +58 -0
  461. package/templates/be/skeleton/src/modules/platform/http-security/errors/http-security.error.ts +3 -0
  462. package/templates/be/skeleton/src/modules/platform/http-security/execution-request.mapper.spec.ts +16 -4
  463. package/templates/be/skeleton/src/modules/platform/http-security/http-security.config.ts +16 -10
  464. package/templates/be/skeleton/src/modules/platform/http-security/http-security.module.ts +3 -1
  465. package/templates/be/skeleton/src/modules/platform/http-security/http-security.options.ts +2 -0
  466. package/templates/be/skeleton/src/modules/platform/http-security/index.ts +5 -0
  467. package/templates/be/skeleton/src/modules/platform/http-security/messages/http-security.messages.ts +12 -5
  468. package/templates/be/skeleton/src/modules/platform/http-security/rate-limit.guard.spec.ts +13 -8
  469. package/templates/be/skeleton/src/modules/platform/http-security/readiness.contracts.ts +5 -0
  470. package/templates/be/skeleton/src/modules/platform/http-security/readiness.decorators.ts +7 -0
  471. package/templates/be/skeleton/src/modules/platform/http-security/readiness.service.spec.ts +86 -0
  472. package/templates/be/skeleton/src/modules/platform/http-security/readiness.service.ts +39 -0
  473. package/templates/be/skeleton/src/modules/platform/http-security/webhook-signature.service.spec.ts +139 -131
  474. package/templates/be/skeleton/src/modules/platform/i18n/index.ts +1 -1
  475. package/templates/be/skeleton/src/modules/platform/logging/json-logger.service.spec.ts +91 -67
  476. package/templates/be/skeleton/src/modules/platform/logging/json-logger.service.ts +5 -4
  477. package/templates/be/skeleton/src/modules/platform/logging/logging.log-events.ts +0 -2
  478. package/templates/be/skeleton/src/modules/platform/primitives/outcome.mapper.spec.ts +20 -6
  479. package/templates/be/skeleton/src/modules/platform/primitives/sequence.policy.spec.ts +16 -4
  480. package/templates/be/skeleton/src/modules/platform/retry/index.ts +3 -0
  481. package/templates/be/skeleton/src/modules/platform/retry/retry.decorators.ts +9 -0
  482. package/templates/be/skeleton/src/modules/platform/retry/retry.module-definition.ts +7 -0
  483. package/templates/be/skeleton/src/modules/platform/retry/retry.module.ts +19 -0
  484. package/templates/be/skeleton/src/modules/platform/retry/retry.options.ts +2 -0
  485. package/templates/be/skeleton/src/modules/platform/retry/retry.port.ts +11 -0
  486. package/templates/be/skeleton/src/modules/platform/retry/system-timer.service.spec.ts +36 -0
  487. package/templates/be/skeleton/src/modules/platform/retry/system-timer.service.ts +10 -0
  488. package/templates/be/skeleton/src/tests/contract/http-security/http-security.contract-spec.ts +54 -0
  489. package/templates/be/skeleton-lite/apps/api/src/api.env.ts +25 -0
  490. package/templates/be/skeleton-lite/apps/api/src/api.options.ts +1 -1
  491. package/templates/be/skeleton-lite/apps/api/src/app.module.ts +18 -58
  492. package/templates/be/skeleton-lite/apps/api/src/compose/api.ts +4 -0
  493. package/templates/be/skeleton-lite/apps/api/src/compose/domain.ts +10 -0
  494. package/templates/be/skeleton-lite/apps/api/src/compose/integrations.ts +16 -0
  495. package/templates/be/skeleton-lite/apps/api/src/compose/platform.ts +47 -0
  496. package/templates/be/skeleton-lite/apps/api/src/compose/system.ts +5 -0
  497. package/templates/be/skeleton-lite/apps/api/src/compose/webhooks.ts +4 -0
  498. package/templates/be/skeleton-lite/apps/api/src/main.ts +5 -13
  499. package/templates/be/skeleton-lite/apps/cli/src/cli.env.ts +17 -0
  500. package/templates/be/skeleton-lite/apps/cli/src/compose/platform.ts +12 -0
  501. package/templates/be/skeleton-lite/src/modules/domain/identity/admission.policy.spec.ts +71 -5
  502. package/templates/be/skeleton-lite/src/modules/domain/identity/admission.policy.ts +29 -2
  503. package/templates/be/skeleton-lite/src/modules/domain/identity/auth.guard.spec.ts +68 -15
  504. package/templates/be/skeleton-lite/src/modules/domain/identity/auth.guard.ts +14 -9
  505. package/templates/be/skeleton-lite/src/modules/domain/identity/identity.contracts.ts +9 -2
  506. package/templates/be/skeleton-lite/src/modules/domain/identity/identity.decorators.ts +16 -1
  507. package/templates/be/skeleton-lite/src/modules/domain/identity/identity.module.ts +19 -4
  508. package/templates/be/skeleton-lite/src/modules/domain/identity/identity.options.ts +5 -1
  509. package/templates/be/skeleton-lite/src/modules/domain/identity/index.ts +2 -1
  510. package/templates/be/skeleton-lite/src/modules/integrations/supabase/errors/supabase.error.ts +0 -3
  511. package/templates/be/skeleton-lite/src/modules/integrations/supabase/supabase.client.spec.ts +17 -5
  512. package/templates/be/skeleton-lite/src/modules/platform/database/database.module.ts +24 -13
  513. package/templates/be/skeleton-lite/src/modules/platform/database/database.port.ts +5 -0
  514. package/templates/be/skeleton-lite/src/modules/platform/database/errors/database.error.ts +16 -0
  515. package/templates/be/skeleton-lite/src/modules/platform/database/index.ts +3 -0
  516. package/templates/be/skeleton-lite/src/modules/platform/http-security/http-security.module.ts +5 -2
  517. package/templates/be/skeleton-lite/src/modules/platform/http-security/index.ts +4 -1
  518. package/templates/be/skeleton-lite/src/modules/platform/i18n/index.ts +1 -1
  519. package/templates/be/skeleton-lite/src/modules/platform/logging/logging.log-events.ts +7 -0
  520. package/templates/be/skeleton-lite/src/tests/fixtures/builders/api.builder.ts +29 -0
  521. package/templates/be/skeleton-lite/src/tests/fixtures/builders/cli.builder.ts +16 -0
  522. package/templates/be/table/api/feature.module-definition.ts.tpl +6 -0
  523. package/templates/be/table/api/feature.module.ts.tpl +2 -1
  524. package/templates/be/table/api/feature.options.ts.tpl +2 -0
  525. package/templates/be/table/api/graphql.module-definition.ts.tpl +6 -0
  526. package/templates/be/table/api/graphql.module.ts.tpl +3 -2
  527. package/templates/be/table/api/graphql.options.ts.tpl +2 -0
  528. package/templates/be/table/migration.sql.tpl +1 -0
  529. package/templates/be/upgrade-full/apps/cli/src/app.module.ts +4 -6
  530. package/templates/be/upgrade-full/apps/cli/src/cli.env.ts +7 -0
  531. package/templates/be/upgrade-full/apps/cli/src/compose/cli.ts +5 -0
  532. package/templates/be/upgrade-full/apps/cli/src/compose/platform.ts +9 -0
  533. package/templates/be/upgrade-full/apps/cli/src/main.ts +3 -0
  534. package/templates/be/upgrade-full/src/features/cli/cli.module.ts +3 -2
  535. package/templates/fe/auth/supabase-client.ts.tpl +34 -0
  536. package/templates/fe/skeleton/apps/__app__/src/app/auth/callback/route.ts +11 -0
  537. package/templates/fe/skeleton/apps/__app__/src/components/blocks/SignInForm/index.tsx +6 -1
  538. package/templates/fe/skeleton/apps/__app__/src/components/leaves/Icon/index.tsx +4 -0
  539. package/templates/fe/skeleton/apps/__app__/src/features/overlays/AppGuideOverlay/component.tsx +31 -0
  540. package/templates/fe/skeleton/apps/__app__/src/features/overlays/AppGuideOverlay/index.tsx +24 -0
  541. package/templates/fe/skeleton/apps/__app__/src/hooks/auth/useSession.ts +6 -1
  542. package/templates/fe/skeleton/apps/__app__/src/hooks/auth/useSignOut.ts +3 -4
  543. package/templates/fe/skeleton/apps/__app__/src/hooks/zustand/appGuide/index.ts +1 -0
  544. package/templates/fe/skeleton/apps/__app__/src/hooks/zustand/appGuide/store.ts +15 -0
  545. package/templates/fe/skeleton/apps/__app__/src/modules/auth/pkce-client.ts +18 -0
  546. package/templates/fe/skeleton/apps/__app__/src/modules/auth/server.ts +19 -0
  547. package/templates/fe/skeleton/apps/app/src/features/pages/AppHomePage/index.tsx +7 -1
  548. package/templates/fe/skeleton/apps/app/src/modules/i18n/messages/en.json +57 -0
  549. package/templates/fe/skeleton/apps/app/src/modules/i18n/messages/vi.json +6 -0
  550. package/templates/fe/skeleton/apps/landing/src/modules/i18n/messages/en.json +31 -0
  551. package/templates/fe/skeleton/packages/__project__-i18n/src/routing.ts +1 -1
  552. package/templates/fe/skeleton/packages/__project__-ui/src/index.ts +1 -0
  553. package/templates/fe/skeleton-lite/apps/__app__/src/modules/auth/client.ts +1 -34
  554. package/templates/fe/skeleton-lite/apps/web/src/features/pages/AppHomePage/index.tsx +7 -1
  555. package/templates/fe/skeleton-lite/apps/web/src/modules/i18n/messages/en.json +57 -0
  556. package/templates/fe/skeleton-lite/apps/web/src/modules/i18n/messages/vi.json +30 -24
  557. package/templates/fe/skeleton-lite/apps/web/src/modules/i18n/routing.ts +1 -1
  558. package/templates/fe/transport/graphql/clients/clients/auth.ts +1 -1
  559. package/templates/fe/transport/graphql/clients/links/locale.ts +2 -2
  560. package/upgrade/check.mjs +3 -1
  561. package/upgrade/file-state.mjs +68 -0
  562. package/upgrade/index.mjs +58 -94
  563. package/runtime/scripts/api/git/log.mjs +0 -5
  564. package/templates/app/skeleton-lite/.starciwork/shell/index.yaml +0 -55
@@ -0,0 +1,70 @@
1
+ // starci app check --coverage-from <lcov>: the optional, read-only coverage proof and its declared boundary (R273).
2
+ import path from 'node:path';
3
+ import { invocationDir } from '../lib/roots.mjs';
4
+ import { isMain } from '../lib/is-main.mjs';
5
+ import { gitOutputOf } from '../lib/git.mjs';
6
+ import { lsFiles } from '../api/git/ls-files.mjs';
7
+ import { loadSlotManifest, readRepoDeclaration } from './slots.mjs';
8
+ import { coverageBelowReport } from './rules/coverage-below.mjs';
9
+
10
+ /** Report for the coordinator's dispatcher; supply its findings as checkRepository's extraFindings. */
11
+ export function coverageFrom({ repoRoot = invocationDir(), coverageFrom: reportPath, files, repo, manifest = loadSlotManifest(), lcov } = {}) {
12
+ const declaration = repo ?? readRepoDeclaration(manifest, repoRoot);
13
+ if (!['app', 'be'].includes(declaration.profile)) throw new Error('coverage proof requires an app root or its be side');
14
+ const tracked = files ?? gitOutputOf(lsFiles(['-z', '--cached', '--exclude-standard'], { dir: repoRoot, maxBuffer: 256 * 1024 * 1024 }), 'git ls-files').split('\0').filter(Boolean);
15
+ return coverageBelowReport({ repoRoot, files: tracked, repo: declaration, manifest, coverageFrom: reportPath, lcov });
16
+ }
17
+
18
+ function setValueOption(opts, flag, value) {
19
+ const key = flag === '--cwd' ? 'cwd' : 'coverageFrom';
20
+ if (value === undefined || !value || value.startsWith('--')) throw new Error(`${flag} needs a value`);
21
+ if (Object.hasOwn(opts, key)) throw new Error(`${flag} may be given only once`);
22
+ opts[key] = value;
23
+ }
24
+
25
+ function parse(argv) {
26
+ const opts = {};
27
+ for (let index = 0; index < argv.length; index += 1) {
28
+ const [flag, inline] = argv[index].split(/=(.*)/su);
29
+ if (flag === '--json' && inline === undefined) opts.json = true;
30
+ else if (['--coverage-from', '--cwd'].includes(flag)) {
31
+ const value = inline ?? argv[++index];
32
+ setValueOption(opts, flag, value);
33
+ } else throw new Error(`unknown argument ${argv[index]}`);
34
+ }
35
+ if (!opts.coverageFrom) throw new Error('usage: starci app check --coverage-from <lcov> [--cwd <app>] [--json]');
36
+ return opts;
37
+ }
38
+
39
+ /** Human-readable measured scope, excluded scope and every unmeasured source file; suitable for normal check output. */
40
+ export function formatCoverageReport(report) {
41
+ const roles = report.scope?.roles ?? [];
42
+ const rolePattern = roles.length === 1 ? roles[0] : '{' + roles.join(',') + '}';
43
+ const lines = [
44
+ `coverage: ${report.ok ? 'passed' : 'refused'} (${report.measured.length} measured logic files; lines/functions/branches at 100% per file)`,
45
+ 'statement coverage is enforced by the unit runner; lcov does not carry statement counts',
46
+ 'measured scope:',
47
+ ...(report.scope?.roots ?? []).map((root) => ` ${report.prefix ?? ''}${root}**/*.${rolePattern}.ts`),
48
+ 'unmeasured scope:',
49
+ ...report.unmeasuredScopes.map((entry) => ` ${entry}${entry.endsWith('/') ? '**' : ''}`),
50
+ 'unmeasured source files:',
51
+ ...(report.unmeasured.length ? report.unmeasured.map((file) => ` ${file}`) : [' (none)']),
52
+ ...report.findings.map((finding) => `${finding.code} ${finding.path}: ${finding.message}`),
53
+ ];
54
+ return `${lines.join('\n')}\n`;
55
+ }
56
+
57
+ /** Standalone coordinator entry point: exit 0 proof passed, 2 proof or invocation refused. */
58
+ export function coverageFromMain(argv, { cwd = invocationDir(), stdout = (text) => process.stdout.write(text), stderr = (text) => process.stderr.write(text), run = coverageFrom } = {}) {
59
+ try {
60
+ const opts = parse(argv);
61
+ const report = run({ repoRoot: path.resolve(cwd, opts.cwd ?? '.'), coverageFrom: opts.coverageFrom });
62
+ stdout(opts.json ? `${JSON.stringify(report, null, 2)}\n` : formatCoverageReport(report));
63
+ return report.ok ? 0 : 2;
64
+ } catch (error) {
65
+ stderr(`starci app check --coverage-from: ${String(error.code ?? error.message).split('\n')[0]}\n`);
66
+ return 2;
67
+ }
68
+ }
69
+
70
+ if (isMain(import.meta.url)) process.exitCode = coverageFromMain(process.argv.slice(2));
@@ -0,0 +1,276 @@
1
+ // app db baseline: schema-only dump, explicit cutover and repair commands; preview writes nothing.
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+ import pg from 'libpg-query';
5
+ import { invocationDir } from '../lib/roots.mjs';
6
+ import { sqlStringParts as names, walkSqlAst as walk, sqlParser } from '../lib/sql-ast.mjs';
7
+ import { databaseDump } from '../api/supabase/database-dump.mjs';
8
+ import { databaseQuery } from '../api/supabase/database-query.mjs';
9
+ import { migrationRepair } from '../api/supabase/migration-repair.mjs';
10
+ import { migrationStamp, isMigrationName, databaseSqlTokens, databaseExecutableTokens } from './rules/database-migrations.mjs';
11
+ import { baselineMarkers, baselineSourceFindings } from './rules/database-baseline.mjs';
12
+ import { databaseQueueDeclarations } from './rules/database-queues.mjs';
13
+ import { MIGRATIONS_DIR, MIGRATIONS_ARCHIVE_DIR } from './rules/database-constants.mjs';
14
+ import { dbPlanMain } from './db-plan-verb.mjs';
15
+
16
+ await pg.loadModule();
17
+
18
+ export class DbVerbError extends Error {}
19
+ const LINKED_PASSWORD_ENV = 'SUPABASE_DB_PASSWORD';
20
+ const TELEMETRY_DISABLED_ENV = 'SUPABASE_TELEMETRY_DISABLED';
21
+ const TELEMETRY_DEBUG_ENV = 'SUPABASE_TELEMETRY_DEBUG';
22
+ const CLI_DEBUG_ENV = 'SUPABASE_DEBUG';
23
+
24
+ /** Strict shared options; unknown flags never reach the database CLI. */
25
+ export function dbOptions(argv, allowed) {
26
+ const opts = {};
27
+ for (let index = 0; index < argv.length; index += 1) {
28
+ const [flag, inline] = argv[index].split(/=(.*)/s);
29
+ const name = flag.slice(2);
30
+ if (!flag.startsWith('--') || !allowed.includes(name) || Object.hasOwn(opts, name)) throw new DbVerbError('unknown or duplicate database option');
31
+ if (['apply', 'local', 'json'].includes(name)) {
32
+ if (inline !== undefined && !['true', 'false'].includes(inline)) throw new DbVerbError(`${flag} expects true or false`);
33
+ opts[name] = inline !== 'false';
34
+ } else {
35
+ const value = inline ?? argv[++index];
36
+ if (!value || value.startsWith('--')) throw new DbVerbError(`${flag} requires a value`);
37
+ opts[name] = value;
38
+ }
39
+ }
40
+ return opts;
41
+ }
42
+
43
+ export function dbAppRoot(cwd, opts) {
44
+ const root = path.resolve(cwd, opts.cwd ?? '.');
45
+ let declaration;
46
+ try { declaration = JSON.parse(fs.readFileSync(path.join(root, 'hfs.json'), 'utf8')); } catch { throw new DbVerbError('run at an app root containing hfs.json'); }
47
+ if (declaration.kind !== 'app' || !['lite', 'full'].includes(declaration.edition ?? 'full')) throw new DbVerbError('database verbs require a lite or full app');
48
+ return root;
49
+ }
50
+
51
+ /** Immediate SQL files only, never secrets or Supabase's temporary credential files. */
52
+ export function dbSqlFiles(root, directory = MIGRATIONS_DIR) {
53
+ const dir = path.join(root, directory);
54
+ if (!fs.existsSync(dir)) return [];
55
+ return fs.readdirSync(dir, { withFileTypes: true }).filter((entry) => entry.isFile() && entry.name.endsWith('.sql')).map((entry) => {
56
+ const match = /^(\d{14})_/.exec(entry.name);
57
+ if (!isMigrationName(entry.name) || !match || migrationStamp(match[1]) === null) throw new DbVerbError('migration filenames must carry valid UTC stamps');
58
+ return { file: `${directory}/${entry.name}`, name: entry.name, stamp: match[1] };
59
+ }).sort((left, right) => left.name < right.name ? -1 : Number(left.name > right.name));
60
+ }
61
+
62
+ /** Supabase adapter uses supabaseSpawn's (repoRoot, args, options) contract; raw failures are private. */
63
+ const DB_CALLS = { 'db dump': databaseDump, 'db query': databaseQuery, 'migration repair': migrationRepair };
64
+
65
+ function runDatabaseCommand(root, args, options) {
66
+ const call = DB_CALLS[args.slice(0, 2).join(' ')];
67
+ if (!call) throw new DbVerbError('unsupported database command');
68
+ return call(root, args.slice(2), options);
69
+ }
70
+
71
+ export function runDbSupabase(root, args, ports = {}) {
72
+ const env = ports.env ?? process.env;
73
+ // The linked CLI can mint a temporary remote login when no existing password is supplied.
74
+ // Check custody presence only; the value stays within the CLI environment.
75
+ if (args.includes('--linked') && !ports.runSupabase && !ports.run && (typeof env[LINKED_PASSWORD_ENV] !== 'string' || env[LINKED_PASSWORD_ENV].length === 0)) throw new DbVerbError('linked database operations require an existing SUPABASE_DB_PASSWORD in the CLI environment');
76
+ const cliEnv = { ...env, [TELEMETRY_DISABLED_ENV]: '1', [TELEMETRY_DEBUG_ENV]: '0', [CLI_DEBUG_ENV]: '0' };
77
+ const result = (ports.runSupabase ?? runDatabaseCommand)(root, args, { env: cliEnv, ...(ports.run ? { run: ports.run } : {}) });
78
+ if (result?.error || !(result?.ok === true || result?.status === 0)) throw new DbVerbError('Supabase command failed; inspect the CLI privately');
79
+ return String(result.stdout ?? '');
80
+ }
81
+
82
+ export const repairArgs = (stamp, status, local) => ['migration', 'repair', stamp, '--status', status, local ? '--local' : '--linked'];
83
+ export const dbFailure = (verb, error, stderr) => { stderr(`starci app db ${verb}: ${error instanceof DbVerbError ? error.message : 'operation failed; database output was withheld'}\n`); return 2; };
84
+
85
+ function baselinePlan(root, opts, now) {
86
+ const active = dbSqlFiles(root);
87
+ if (!active.length) throw new DbVerbError('baseline requires an existing active migration chain');
88
+ const archived = dbSqlFiles(root, MIGRATIONS_ARCHIVE_DIR);
89
+ const oldStamps = [...active, ...archived].map((entry) => entry.stamp);
90
+ const last = oldStamps.sort((left, right) => left.localeCompare(right)).at(-1);
91
+ const stamp = opts.stamp ?? now().toISOString().replace(/\D/g, '').slice(0, 14);
92
+ if (!/^\d{14}$/.test(stamp) || migrationStamp(stamp) === null || stamp <= last) throw new DbVerbError('--stamp must be a valid UTC version after the archive boundary');
93
+ const file = `${MIGRATIONS_DIR}/${stamp}_baseline.sql`;
94
+ if (fs.existsSync(path.join(root, file)) || active.some((entry) => fs.existsSync(path.join(root, MIGRATIONS_ARCHIVE_DIR, entry.name)))) throw new DbVerbError('baseline would overwrite an existing migration');
95
+ return { active, last, stamp, file };
96
+ }
97
+
98
+ const EXCLUDED_SCHEMAS = new Set(['information_schema', '_analytics', '_realtime', '_supavisor', 'auth', 'etl', 'extensions', 'pgbouncer', 'realtime', 'storage', 'supabase_functions', 'supabase_migrations', 'cron', 'dbdev', 'graphql', 'graphql_public', 'net', 'pgmq', 'pgmq_public', 'pgsodium', 'pgsodium_masks', 'pgtle', 'repack', 'tiger', 'tiger_data', 'topology', 'vault']);
99
+ const ROLE_OPTIONS = new Set(['canlogin', 'inherit', 'superuser', 'createdb', 'createrole', 'isreplication', 'bypassrls', 'connectionlimit']);
100
+ const identifier = (name) => `"${name.replaceAll('"', '""')}"`;
101
+ const literal = (value) => `'${value.replaceAll("'", "''")}'`;
102
+ const excludedSchema = (name) => EXCLUDED_SCHEMAS.has(name) || /^(?:pg_|timescaledb_|_timescaledb_)/.test(name ?? '');
103
+
104
+ /** Inventory source paths only; no credential, generated or vendor tree is traversed. */
105
+ function baselineFiles(root) {
106
+ const files = [];
107
+ const visit = (directory) => {
108
+ const full = path.join(root, directory);
109
+ if (!fs.existsSync(full)) return;
110
+ for (const entry of fs.readdirSync(full, { withFileTypes: true })) {
111
+ if (entry.name.startsWith('.') || entry.name === 'node_modules') continue;
112
+ const file = `${directory}/${entry.name}`;
113
+ if (entry.isDirectory()) visit(file);
114
+ else if (entry.isFile() && /\.(?:sql|[cm]?[jt]sx?)$/.test(entry.name)) files.push(file);
115
+ }
116
+ };
117
+ for (const directory of ['supabase', 'be/src', 'be/apps', 'src', 'apps']) visit(directory);
118
+ return files;
119
+ }
120
+
121
+ const parsed = sqlParser(pg, () => { throw new DbVerbError('migration SQL could not be prepared safely for a baseline'); });
122
+
123
+ function statementSql(text, entry) {
124
+ const bytes = Buffer.from(text);
125
+ const start = entry.stmt_location ?? 0;
126
+ const statement = bytes.subarray(start, entry.stmt_len ? start + entry.stmt_len : bytes.length).toString('utf8');
127
+ return `${statement.slice(databaseSqlTokens(statement)[0]?.index ?? 0).trim().replace(/;\s*$/, '')};\n`;
128
+ }
129
+
130
+ function ownedSchemas(node) {
131
+ const schemas = [node.relation?.schemaname, node.table?.schemaname, node.schemaname, node.newschema];
132
+ for (const field of ['funcname', 'typeName', 'typename', 'type', 'object', 'domainname', 'collname', 'opfamilyname', 'opclassname']) {
133
+ const parts = names(Array.isArray(node[field]) ? node[field] : node[field]?.names ?? node[field]?.List?.items);
134
+ if (parts.length > 1) schemas.push(parts[0]);
135
+ }
136
+ for (const object of node.objects ?? []) {
137
+ const parts = names(object.List?.items ?? object.ObjectWithArgs?.objname);
138
+ schemas.push(object.RangeVar?.schemaname);
139
+ if (parts.length > 1 || /^OBJECT_SCHEMA$/.test(node.objtype ?? node.removeType ?? '')) schemas.push(parts[0]);
140
+ }
141
+ return schemas;
142
+ }
143
+
144
+ function excludedOwnership(stmt) {
145
+ if (ownedSchemas(stmt).some(excludedSchema)) return true;
146
+ let unsupported = false;
147
+ walk(stmt, (node) => {
148
+ if (node.DefElem?.defname === 'schemas' && names(node.DefElem.arg?.List?.items).some(excludedSchema)) unsupported = true;
149
+ if (node.FuncCall && names(node.FuncCall.funcname)[0] === 'vault') unsupported = true;
150
+ });
151
+ if (stmt.name === 'search_path') unsupported ||= (stmt.args ?? []).some((node) => excludedSchema(node.A_Const?.sval?.sval));
152
+ return unsupported;
153
+ }
154
+
155
+ function dataMutation(stmt) {
156
+ if (stmt.CreateFunctionStmt || stmt.CreateTrigStmt || stmt.CreatePolicyStmt || stmt.AlterPolicyStmt || stmt.CreateStmt || stmt.ViewStmt) return false;
157
+ let mutation = false;
158
+ walk(stmt, (node) => { if (['InsertStmt', 'UpdateStmt', 'DeleteStmt', 'MergeStmt', 'CopyStmt', 'TruncateStmt', 'IntoClause'].some((key) => node[key])) mutation = true; });
159
+ return mutation;
160
+ }
161
+
162
+ function unsupportedCall(call) {
163
+ const name = names(call.funcname).join('.');
164
+ const args = (call.args ?? []).map((arg) => arg.NamedArgExpr?.arg ?? arg);
165
+ if (name === 'pgmq.create' || name === 'pgmq.drop_queue') return args.length !== 1 || !args[0]?.A_Const?.sval;
166
+ if (name === 'cron.schedule' || name === 'cron.unschedule') return false;
167
+ if (name === 'pg_catalog.set_config') return args[0]?.A_Const?.sval?.sval !== 'search_path' || args[1]?.A_Const?.sval?.sval !== '';
168
+ return true;
169
+ }
170
+
171
+ function sourceExecution(stmt) {
172
+ if (stmt.CallStmt) return true;
173
+ if (!stmt.SelectStmt) return false;
174
+ let unsupported = false;
175
+ walk(stmt, (node) => { if (node.FuncCall && unsupportedCall(node.FuncCall)) unsupported = true; });
176
+ return unsupported;
177
+ }
178
+
179
+ function unsupportedDo(node) {
180
+ const body = (node.args ?? []).find((item) => item.DefElem?.defname === 'as')?.DefElem?.arg?.String?.sval ?? '';
181
+ const tokens = databaseExecutableTokens(body);
182
+ const unsupported = tokens.some((token, index) => {
183
+ const next = tokens[index + 1]?.v;
184
+ return (token.t === 'word' && ['execute', 'role', 'user', 'extension', 'insert', 'update', 'delete', 'merge', 'copy', 'truncate'].includes(token.v))
185
+ || (excludedSchema(token.v) && next === '.' && !['pgmq', 'cron'].includes(token.v));
186
+ });
187
+ if (unsupported) return true;
188
+ let tree;
189
+ try { tree = pg.parsePlPgSQLSync(`do ${literal(body)};`); } catch { return true; }
190
+ let effects = false;
191
+ walk(tree, (item) => {
192
+ if (item.PLpgSQL_expr?.parseMode !== 0) return;
193
+ const sql = parsed(item.PLpgSQL_expr.query);
194
+ if ((sql.stmts ?? []).some((entry) => dataMutation(entry.stmt) || sourceExecution(entry.stmt))) effects = true;
195
+ });
196
+ return effects;
197
+ }
198
+
199
+ function roleDeclaration(stmt, sql, roles) {
200
+ const create = stmt.CreateRoleStmt;
201
+ const alter = stmt.AlterRoleStmt;
202
+ const setting = stmt.AlterRoleSetStmt;
203
+ const declaration = create ?? alter ?? setting;
204
+ if (!declaration) return false;
205
+ const name = create?.role ?? declaration.role?.rolename;
206
+ if (!name || name.startsWith('pg_') || (declaration.options ?? []).some((option) => !ROLE_OPTIONS.has(option.DefElem?.defname))) throw new DbVerbError('baseline role declarations require static non-secret role attributes');
207
+ if (setting && setting.setstmt?.name !== 'search_path') throw new DbVerbError('baseline supports only non-secret search_path role settings');
208
+ if (create) roles.set(name, [sql]);
209
+ else {
210
+ if (!roles.has(name)) throw new DbVerbError('baseline cannot reconstruct alterations to a role it does not declare');
211
+ roles.get(name).push(sql);
212
+ }
213
+ return true;
214
+ }
215
+
216
+ function extensionDeclaration(stmt, sql, extensions) {
217
+ if (stmt.CreateExtensionStmt) {
218
+ extensions.set(stmt.CreateExtensionStmt.extname, sql.replace(/\bcreate\s+extension\s+(?!if\s+not\s+exists\b)/i, 'create extension if not exists '));
219
+ return true;
220
+ }
221
+ if (!/^OBJECT_EXTENSION$/.test(stmt.DropStmt?.removeType ?? '')) return false;
222
+ for (const object of stmt.DropStmt.objects ?? []) extensions.delete(names(object.List?.items)[0]);
223
+ return true;
224
+ }
225
+
226
+ function baselineStatement(state, stmt, sql) {
227
+ if (roleDeclaration(stmt, sql, state.roles) || extensionDeclaration(stmt, sql, state.extensions)) return;
228
+ if (stmt.DropRoleStmt) { for (const role of stmt.DropRoleStmt.roles ?? []) state.roles.delete(role.RoleSpec?.rolename); return; }
229
+ if (stmt.GrantRoleStmt || stmt.AlterExtensionStmt || stmt.AlterExtensionContentsStmt || /^OBJECT_ROLE$/.test(stmt.RenameStmt?.renameType ?? '')) throw new DbVerbError('baseline cannot reconstruct role membership, role renames or extension alterations');
230
+ if (dataMutation(stmt) || sourceExecution(stmt)) throw new DbVerbError('baseline cannot reconstruct migration-owned data changes or custom execution from a schema-only dump');
231
+ const node = Object.values(stmt)[0];
232
+ const queueGrant = stmt.GrantStmt && (node.objects ?? []).every((object) => object.RangeVar?.schemaname === 'pgmq');
233
+ if ((excludedOwnership(node) && !queueGrant) || (stmt.DoStmt && unsupportedDo(node))) throw new DbVerbError('baseline cannot reconstruct application changes to excluded managed schemas or dynamic operational declarations');
234
+ }
235
+
236
+ function baselineMetadata(root, plan, files) {
237
+ const queues = databaseQueueDeclarations({ repoRoot: root, files, all: files });
238
+ if (queues.findings.length) throw new DbVerbError('baseline requires valid migration-owned queues, grants and cron declarations');
239
+ const state = { roles: new Map(), extensions: new Map() };
240
+ for (const migration of plan.active) {
241
+ const text = fs.readFileSync(path.join(root, migration.file), 'utf8');
242
+ for (const entry of parsed(text).stmts ?? []) baselineStatement(state, entry.stmt, statementSql(text, entry));
243
+ }
244
+ return { ...queues, roles: [...state.roles.values()].flat().join(''), extensions: [...state.extensions.values()].join('') };
245
+ }
246
+
247
+ function metadataSql(metadata) {
248
+ const queueSql = metadata.queues.map((queue) => `select pgmq.create(${literal(queue)});\n`).join('');
249
+ const grants = (metadata.grants ?? []).map((grant) => `grant ${grant.privileges.join(', ')} on table ${grant.tables.map((table) => table.split('.').map(identifier).join('.')).join(', ')} to ${grant.roles.map(identifier).join(', ')}${grant.grantOption ? ' with grant option' : ''};\n`).join('');
250
+ const cron = metadata.cron.map((job) => `select cron.schedule(${literal(job.name)}, ${literal(job.schedule)}, ${literal(job.command)});\n`).join('');
251
+ return `${queueSql}${grants}${cron}`;
252
+ }
253
+
254
+ /** Numeric CLI exit code, injectable IO/clock/CLI; neither files nor history change without --apply. */
255
+ export function dbBaselineMain(argv, { cwd = invocationDir(), stdout = (text) => process.stdout.write(text), stderr = (text) => process.stderr.write(text), now = () => new Date(), ...ports } = {}) {
256
+ try {
257
+ const opts = dbOptions(argv, ['cwd', 'stamp', 'apply', 'local']);
258
+ const root = dbAppRoot(cwd, opts);
259
+ const plan = baselinePlan(root, opts, now);
260
+ const files = baselineFiles(root);
261
+ if (baselineSourceFindings({ repoRoot: root, files, all: files, git: ports.git }).length) throw new DbVerbError('baseline sources and archives must match committed migration history');
262
+ const metadata = baselineMetadata(root, plan, files);
263
+ if (dbPlanMain(['--json', ...(opts.local ? ['--local'] : [])], { ...ports, cwd: root, stdout: () => {}, stderr: () => {} }) !== 0) throw new DbVerbError('baseline requires a clean database plan with all local migrations already applied');
264
+ const schema = runDbSupabase(root, ['db', 'dump', opts.local ? '--local' : '--linked'], ports);
265
+ if (!schema.trim() || baselineMarkers(schema).length || /\bvault\.(?:create_secret|update_secret)\s*\(|\bpassword\s+'[^']+'/i.test(schema)) throw new DbVerbError('schema dump is empty or contains unsupported secret/baseline statements');
266
+ const repairs = [...plan.active.map((entry) => repairArgs(entry.stamp, 'reverted', opts.local)), repairArgs(plan.stamp, 'applied', opts.local)];
267
+ stdout(`${opts.apply ? 'apply' : 'preview'} baseline ${plan.file}; archive ${plan.active.length} migrations\n`);
268
+ for (const args of repairs) stdout(`supabase ${args.join(' ')}\n`);
269
+ if (!opts.apply) return 0;
270
+ for (const args of repairs) runDbSupabase(root, args, ports);
271
+ fs.mkdirSync(path.join(root, MIGRATIONS_ARCHIVE_DIR), { recursive: true });
272
+ fs.writeFileSync(path.join(root, plan.file), `-- starci:baseline ${plan.last}\n${metadata.extensions}${metadata.roles}${schema.replace(/^\s+/, '')}\n${metadataSql(metadata)}`, { flag: 'wx' });
273
+ for (const entry of plan.active) fs.renameSync(path.join(root, entry.file), path.join(root, MIGRATIONS_ARCHIVE_DIR, entry.name));
274
+ return 0;
275
+ } catch (error) { return dbFailure('baseline', error, stderr); }
276
+ }
@@ -0,0 +1,254 @@
1
+ // app db plan: compare migration-owned state without fetching commands or Vault values.
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+ import { createHash } from 'node:crypto';
5
+ import pg from 'libpg-query';
6
+ import { parse as parseToml } from 'smol-toml';
7
+ import { invocationDir } from '../lib/roots.mjs';
8
+ import { sqlStringParts, walkSqlAst as walk, sqlParser, sqlCallArgument as argument, sqlStringLiteral as literal } from '../lib/sql-ast.mjs';
9
+ import { DbVerbError, dbOptions, dbAppRoot, dbSqlFiles, runDbSupabase, dbFailure } from './db-baseline-verb.mjs';
10
+
11
+ await pg.loadModule();
12
+
13
+ // The pinned Supabase CLI uses positional SQL, --output json and --agent no for a plain rows array.
14
+ // Contract: supabase/cli apps/cli/src/commands/db/query/{query.command,query.handler}.ts at the pinned CLI tag.
15
+ const PRESENCE_SQL = `select
16
+ pg_catalog.to_regclass('supabase_migrations.schema_migrations') is not null as migrations,
17
+ pg_catalog.to_regclass('cron.job') is not null as cron,
18
+ pg_catalog.to_regclass('pgmq.meta') is not null as queues,
19
+ pg_catalog.to_regclass('vault.secrets') is not null as vault;`;
20
+ const INVENTORIES = {
21
+ migrations: 'select version from supabase_migrations.schema_migrations order by version;',
22
+ cron: 'select jobname as name, schedule, pg_catalog.md5(command) as "commandHash", active from cron.job order by jobid;',
23
+ queues: 'select queue_name as name from pgmq.meta order by queue_name;',
24
+ vault: 'select name from vault.secrets order by name;',
25
+ };
26
+ const names = (nodes) => sqlStringParts(nodes).join('.');
27
+ const hash = (command) => createHash('md5').update(command).digest('hex');
28
+ const OWNERSHIP_CALLS = new Set(['pgmq.create', 'pgmq.drop_queue', 'cron.schedule', 'cron.unschedule', 'cron.schedule_in_database', 'cron.alter_job']);
29
+
30
+ const parseSql = sqlParser(pg, () => { throw new DbVerbError('migration SQL could not be inventoried safely'); });
31
+
32
+ function requiredLiteral(call, index, aliases) {
33
+ const value = literal(argument(call, index, aliases));
34
+ if (value === null || value.length === 0) throw new DbVerbError('queue and cron ownership requires literal migration declarations');
35
+ return value;
36
+ }
37
+
38
+ function inventoryCall(state, call) {
39
+ const name = names(call.funcname);
40
+ if (name === 'pgmq.create') {
41
+ state.queues.set(requiredLiteral(call, 0, ['queue_name']), null);
42
+ } else if (name === 'pgmq.drop_queue') {
43
+ state.queues.delete(requiredLiteral(call, 0, ['queue_name']));
44
+ } else if (name === 'cron.schedule') {
45
+ const job = requiredLiteral(call, 0, ['job_name']);
46
+ const schedule = requiredLiteral(call, 1, ['schedule']);
47
+ const command = requiredLiteral(call, 2, ['command']);
48
+ state.cron.set(job, { schedule, commandHash: hash(command), active: true });
49
+ } else if (name === 'cron.unschedule') {
50
+ state.cron.delete(requiredLiteral(call, 0, ['job_name']));
51
+ } else if (name === 'cron.schedule_in_database' || name === 'cron.alter_job') {
52
+ throw new DbVerbError('database plan requires cron.schedule and named cron.unschedule ownership');
53
+ }
54
+ }
55
+
56
+ function ownershipCalls(tree) {
57
+ const calls = [];
58
+ walk(tree, (node) => { if (node.FuncCall && OWNERSHIP_CALLS.has(names(node.FuncCall.funcname))) calls.push(node.FuncCall); });
59
+ return calls;
60
+ }
61
+
62
+ function flatDeclaration(tree, call) {
63
+ const node = tree.SelectStmt ?? (tree.stmts?.length === 1 && tree.stmts[0].stmt.SelectStmt);
64
+ const constrained = ['fromClause', 'withClause', 'whereClause', 'intoClause', 'limitCount', 'limitOffset', 'groupClause', 'havingClause', 'distinctClause', 'windowClause'];
65
+ return node && /^SETOP_NONE$/.test(node.op) && node.targetList?.length === 1
66
+ && !constrained.some((key) => node[key]) && node.targetList[0].ResTarget?.val?.FuncCall === call;
67
+ }
68
+
69
+ function inventoryTree(state, tree) {
70
+ const calls = ownershipCalls(tree);
71
+ if (!calls.length) return;
72
+ if (calls.length !== 1 || !flatDeclaration(tree, calls[0])) throw new DbVerbError('database ownership requires one unconditional migration SELECT per declaration');
73
+ inventoryCall(state, calls[0]);
74
+ }
75
+
76
+ function conditionalBlock(item) {
77
+ if (item.PLpgSQL_stmt_block?.exceptions) return true;
78
+ return Object.keys(item).some((key) => key.startsWith('PLpgSQL_stmt_')
79
+ && !['PLpgSQL_stmt_block', 'PLpgSQL_stmt_perform', 'PLpgSQL_stmt_execsql'].includes(key)
80
+ && !(key === 'PLpgSQL_stmt_return' && item[key].lineno === undefined));
81
+ }
82
+
83
+ function inventoryDo(state, node) {
84
+ const option = (key) => (node.args ?? []).find((item) => item.DefElem?.defname === key)?.DefElem?.arg?.String?.sval;
85
+ if (option('language') && option('language') !== 'plpgsql') throw new DbVerbError('database plan requires statically readable migration blocks');
86
+ const body = option('as') ?? '';
87
+ let delimiter = '$starci_plan$';
88
+ while (body.includes(delimiter)) delimiter = `${delimiter.slice(0, -1)}_$`;
89
+ let tree;
90
+ try { tree = pg.parsePlPgSQLSync(`create function starci_plan_probe() returns void language plpgsql as ${delimiter}${body}${delimiter};`); }
91
+ catch { throw new DbVerbError('migration block could not be inventoried safely'); }
92
+ const statements = [];
93
+ let conditional = false;
94
+ walk(tree, (item) => {
95
+ conditional ||= conditionalBlock(item);
96
+ if (item.PLpgSQL_stmt_dynexecute) throw new DbVerbError('dynamic migration blocks cannot prove database inventory');
97
+ if (item.PLpgSQL_expr?.parseMode === 0) statements.push(parseSql(item.PLpgSQL_expr.query));
98
+ });
99
+ if (conditional && statements.some((statement) => ownershipCalls(statement).length)) throw new DbVerbError('conditional migration blocks cannot prove database inventory');
100
+ for (const statement of statements) inventoryTree(state, statement);
101
+ }
102
+
103
+ function quotedEnd(text, start) {
104
+ const quote = text[start];
105
+ const delimiter = text.startsWith(quote.repeat(3), start) ? quote.repeat(3) : quote;
106
+ let cursor = start + delimiter.length;
107
+ while (cursor < text.length) {
108
+ if (quote === '"' && text[cursor] === '\\') { cursor += 2; continue; }
109
+ if (text.startsWith(delimiter, cursor)) {
110
+ let end = cursor + delimiter.length;
111
+ if (delimiter.length === 3) while (text[end] === quote) end += 1;
112
+ return end;
113
+ }
114
+ cursor += 1;
115
+ }
116
+ throw new DbVerbError('Supabase config contains an unterminated string');
117
+ }
118
+
119
+ // Advance over an assignment value without collecting, decoding or resolving its content.
120
+ function valueEnd(text, start) {
121
+ let cursor = start;
122
+ let depth = 0;
123
+ while (cursor < text.length) {
124
+ const character = text[cursor];
125
+ if (character === '"' || character === "'") { cursor = quotedEnd(text, cursor); continue; }
126
+ if (character === '#') {
127
+ cursor = text.indexOf('\n', cursor);
128
+ if (cursor < 0) return text.length;
129
+ if (depth === 0) return cursor + 1;
130
+ } else if (character === '[' || character === '{') depth += 1;
131
+ else if (character === ']' || character === '}') depth -= 1;
132
+ else if (character === '\n' && depth === 0) return cursor + 1;
133
+ cursor += 1;
134
+ }
135
+ return cursor;
136
+ }
137
+
138
+ function assignmentEnd(text, start) {
139
+ let cursor = start;
140
+ while (cursor < text.length && !['=', '\n', '#'].includes(text[cursor])) {
141
+ if (text[cursor] === '"' || text[cursor] === "'") cursor = quotedEnd(text, cursor);
142
+ else cursor += 1;
143
+ }
144
+ return cursor;
145
+ }
146
+
147
+ function vaultKeys(table, key) {
148
+ // The TOML parser sees only the table/key syntax and an invented empty value.
149
+ let metadata;
150
+ try { metadata = parseToml(`${table}\n${key} = ""`); }
151
+ catch { throw new DbVerbError('Supabase config keys could not be inventoried safely'); }
152
+ const vault = metadata.db?.vault ?? {};
153
+ if (typeof vault !== 'object' || Object.values(vault).some((value) => typeof value !== 'string')) throw new DbVerbError('Vault config requires individual named keys');
154
+ return Object.keys(vault);
155
+ }
156
+
157
+ function configToken(text, cursor, table) {
158
+ if (/\s/.test(text[cursor])) return { cursor: cursor + 1, table, keys: [] };
159
+ if (text[cursor] === '#' || text[cursor] === '[') {
160
+ const end = text.indexOf('\n', cursor);
161
+ const line = text.slice(cursor, end < 0 ? undefined : end);
162
+ return { cursor: end < 0 ? text.length : end + 1, table: text[cursor] === '[' ? line : table, keys: [] };
163
+ }
164
+ const end = assignmentEnd(text, cursor);
165
+ if (text[end] !== '=') throw new DbVerbError('Supabase config keys could not be inventoried safely');
166
+ return { cursor: valueEnd(text, end + 1), table, keys: vaultKeys(table, text.slice(cursor, end)) };
167
+ }
168
+
169
+ function configVaultNames(root) {
170
+ const file = path.join(root, 'supabase/config.toml');
171
+ if (!fs.existsSync(file)) return new Map();
172
+ const text = fs.readFileSync(file, 'utf8');
173
+ const configured = new Map();
174
+ let token = { cursor: 0, table: '' };
175
+ while (token.cursor < text.length) {
176
+ token = configToken(text, token.cursor, token.table);
177
+ for (const name of token.keys) {
178
+ if (configured.has(name)) throw new DbVerbError('Vault config names must be unique');
179
+ configured.set(name, null);
180
+ }
181
+ }
182
+ return configured;
183
+ }
184
+
185
+ function repositoryState(root) {
186
+ const migrations = dbSqlFiles(root);
187
+ const state = { migrations: new Map(migrations.map((entry) => [entry.stamp, null])), cron: new Map(), queues: new Map(), vault: configVaultNames(root) };
188
+ if (state.migrations.size !== migrations.length) throw new DbVerbError('active migrations must have unique versions');
189
+ for (const entry of migrations) {
190
+ const tree = parseSql(fs.readFileSync(path.join(root, entry.file), 'utf8'));
191
+ for (const statement of tree.stmts ?? []) {
192
+ // Creating a function does not execute its body. Inert SQL strings never declare remote state.
193
+ if (statement.stmt.CreateFunctionStmt) continue;
194
+ if (statement.stmt.DoStmt) inventoryDo(state, statement.stmt.DoStmt);
195
+ else inventoryTree(state, statement.stmt);
196
+ }
197
+ }
198
+ return state;
199
+ }
200
+
201
+ function queryRows(root, sql, local, ports) {
202
+ // The pinned CLI otherwise creates this cache in its linked-query finalizer.
203
+ // Existing metadata is sufficient; never inspect the cache or credential files.
204
+ if (!local && !ports.runSupabase && !ports.run && !fs.existsSync(path.join(root, 'supabase/.temp/linked-project.json'))) throw new DbVerbError('link the existing project before running a read-only database plan');
205
+ const output = runDbSupabase(root, ['db', 'query', sql, local ? '--local' : '--linked', '--output', 'json', '--agent', 'no'], ports);
206
+ let rows;
207
+ try { rows = JSON.parse(output); } catch { throw new DbVerbError('Supabase returned unreadable inventory; database output was withheld'); }
208
+ if (!Array.isArray(rows) || rows.some((row) => !row || typeof row !== 'object' || Array.isArray(row))) throw new DbVerbError('Supabase returned an invalid inventory; database output was withheld');
209
+ return rows;
210
+ }
211
+
212
+ function remoteState(root, local, ports) {
213
+ const rows = queryRows(root, PRESENCE_SQL, local, ports);
214
+ if (rows.length !== 1 || Object.keys(INVENTORIES).some((key) => typeof rows[0][key] !== 'boolean')) throw new DbVerbError('Supabase returned invalid inventory availability');
215
+ const state = {};
216
+ for (const [key, sql] of Object.entries(INVENTORIES)) state[key] = rows[0][key] ? queryRows(root, sql, local, ports) : [];
217
+ return state;
218
+ }
219
+
220
+ function compare(expected, actual, kind) {
221
+ const unmatched = new Set(expected.keys());
222
+ let unexpected = 0;
223
+ let changed = 0;
224
+ for (const row of actual) {
225
+ const key = kind === 'migrations' ? row.version : row.name;
226
+ if (key !== null && (typeof key !== 'string' || key.length === 0)) throw new DbVerbError('Supabase returned an invalid inventory identifier');
227
+ if (kind === 'cron' && (typeof row.schedule !== 'string' || !/^[a-f0-9]{32}$/.test(row.commandHash ?? '') || typeof row.active !== 'boolean')) throw new DbVerbError('Supabase returned invalid cron metadata');
228
+ if (!unmatched.delete(key)) unexpected += 1;
229
+ else if (kind === 'cron') {
230
+ const desired = expected.get(key);
231
+ if (desired.schedule !== row.schedule || desired.commandHash !== row.commandHash || desired.active !== row.active) changed += 1;
232
+ }
233
+ }
234
+ return { expected: expected.size, remote: actual.length, missing: unmatched.size, unexpected, changed };
235
+ }
236
+
237
+ /** Numeric exit codes: 0 equal, 1 drift, 2 invalid/failed; all IO and CLI calls are injectable. */
238
+ export function dbPlanMain(argv, { cwd = invocationDir(), stdout = (text) => process.stdout.write(text), stderr = (text) => process.stderr.write(text), ...ports } = {}) {
239
+ try {
240
+ const opts = dbOptions(argv, ['cwd', 'local', 'json', 'apply']);
241
+ const root = dbAppRoot(cwd, opts);
242
+ const expected = repositoryState(root);
243
+ const actual = remoteState(root, opts.local, ports);
244
+ const summaries = Object.fromEntries(Object.keys(INVENTORIES).map((kind) => [kind, compare(expected[kind], actual[kind], kind)]));
245
+ const drift = Object.values(summaries).some((item) => item.missing || item.unexpected || item.changed);
246
+ // --apply is accepted for shared verb ergonomics; a plan never mutates history, queues or jobs.
247
+ if (opts.json) stdout(`${JSON.stringify({ readOnly: true, drift, ...summaries })}\n`);
248
+ else {
249
+ stdout(`read-only database plan: ${drift ? 'drift detected' : 'repo and database agree'}\n`);
250
+ for (const [kind, item] of Object.entries(summaries)) stdout(`${kind}: repo ${item.expected}, remote ${item.remote}, missing ${item.missing}, unexpected ${item.unexpected}, changed ${item.changed}\n`);
251
+ }
252
+ return drift ? 1 : 0;
253
+ } catch (error) { return dbFailure('plan', error, stderr); }
254
+ }