@starci/hfs 4.3.0 → 4.4.1

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 (538) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/README.md +1 -1
  3. package/db/migrate.mjs +361 -0
  4. package/db/post-cutover.mjs +136 -0
  5. package/emit/contracts.mjs +2 -2
  6. package/emit/db-types.mjs +12 -3
  7. package/lint/run.mjs +3 -3
  8. package/package.json +4 -1
  9. package/release/gate.mjs +298 -0
  10. package/release/index.mjs +103 -0
  11. package/release/push.mjs +110 -0
  12. package/report/sonar.mjs +1 -1
  13. package/runtime/config.example.yaml +7 -6
  14. package/runtime/engine/config.mjs +10 -18
  15. package/runtime/engine/removed-vocabulary.mjs +0 -2
  16. package/runtime/knowledge/hfs/canon-pins.yaml +11 -11
  17. package/runtime/knowledge/hfs/fe-stack.yaml +22 -1
  18. package/runtime/knowledge/hfs/rules.yaml +2855 -163
  19. package/runtime/knowledge/hfs/slots.yaml +333 -62
  20. package/runtime/knowledge/patterns/be/api.yaml +32 -5
  21. package/runtime/knowledge/patterns/be/cli.yaml +14 -4
  22. package/runtime/knowledge/patterns/be/event-bus.yaml +3 -3
  23. package/runtime/knowledge/patterns/be/jobs.yaml +20 -2
  24. package/runtime/knowledge/patterns/be/projections.yaml +4 -2
  25. package/runtime/knowledge/patterns/be/queues.yaml +11 -3
  26. package/runtime/knowledge/patterns/be/reactors.yaml +20 -1
  27. package/runtime/knowledge/patterns/be/realtime.yaml +25 -3
  28. package/runtime/knowledge/patterns/be/saga.yaml +20 -3
  29. package/runtime/knowledge/patterns/be/webhooks.yaml +23 -4
  30. package/runtime/knowledge/patterns/fe/folder.yaml +1 -0
  31. package/runtime/knowledge/sonar-gate.yaml +1 -1
  32. package/runtime/modules/kernel/failure-codes.yaml +3091 -43
  33. package/runtime/modules/kernel/removed-vocabulary.yaml +30 -0
  34. package/runtime/modules/models/runtimes.yaml +5 -7
  35. package/runtime/modules/schemas/application-stacks.schema.yaml +663 -0
  36. package/runtime/packages/cli/src/catalog.generated.mjs +13769 -0
  37. package/runtime/packages/cli/src/validate-args.mjs +213 -0
  38. package/runtime/scripts/api/git/add.mjs +5 -0
  39. package/runtime/scripts/api/git/init.mjs +7 -0
  40. package/runtime/scripts/api/git/lib.mjs +4 -1
  41. package/runtime/scripts/api/git/tag.mjs +5 -0
  42. package/runtime/scripts/api/node/lib.mjs +20 -0
  43. package/runtime/scripts/api/node/run-node.mjs +6 -0
  44. package/runtime/scripts/api/sops/lib.mjs +42 -1
  45. package/runtime/scripts/api/sops/selected-age-recipient.mjs +7 -0
  46. package/runtime/scripts/api/supabase/database-dump.mjs +5 -0
  47. package/runtime/scripts/api/supabase/database-query.mjs +5 -0
  48. package/runtime/scripts/api/supabase/lib.mjs +44 -0
  49. package/runtime/scripts/api/supabase/migration-repair.mjs +5 -0
  50. package/runtime/scripts/hfs/app-wiring.mjs +201 -0
  51. package/runtime/scripts/hfs/architecture/backend-app-files.mjs +1 -8
  52. package/runtime/scripts/hfs/architecture/backend.mjs +2 -7
  53. package/runtime/scripts/hfs/architecture/client-reaches-server.mjs +6 -3
  54. package/runtime/scripts/hfs/architecture/config.mjs +1 -1
  55. package/runtime/scripts/hfs/architecture/dead-exports-bindings.mjs +149 -0
  56. package/runtime/scripts/hfs/architecture/dead-exports.mjs +9 -41
  57. package/runtime/scripts/hfs/architecture/default-deny.mjs +8 -6
  58. package/runtime/scripts/hfs/architecture/frontend-world.mjs +1 -1
  59. package/runtime/scripts/hfs/architecture/function-spec-providers.mjs +153 -0
  60. package/runtime/scripts/hfs/architecture/hfs.mjs +3 -3
  61. package/runtime/scripts/hfs/architecture/index.mjs +1 -1
  62. package/runtime/scripts/hfs/architecture/module-per-transport.mjs +28 -13
  63. package/runtime/scripts/hfs/architecture/next-data-inference.mjs +176 -0
  64. package/runtime/scripts/hfs/architecture/next-data-symbols.mjs +26 -0
  65. package/runtime/scripts/hfs/architecture/next-data.mjs +31 -23
  66. package/runtime/scripts/hfs/architecture/reachability.mjs +14 -0
  67. package/runtime/scripts/hfs/architecture/register-once.mjs +2 -2
  68. package/runtime/scripts/hfs/architecture/source-names.mjs +48 -3
  69. package/runtime/scripts/hfs/architecture/supabase-ast.mjs +211 -11
  70. package/runtime/scripts/hfs/architecture/supabase-be.mjs +1 -1
  71. package/runtime/scripts/hfs/architecture/supabase.mjs +59 -21
  72. package/runtime/scripts/hfs/architecture/surface.mjs +2 -1
  73. package/runtime/scripts/hfs/architecture/tiers.mjs +4 -4
  74. package/runtime/scripts/hfs/architecture/unit-spec-providers.mjs +8 -2
  75. package/runtime/scripts/hfs/architecture.mjs +1 -1
  76. package/runtime/scripts/hfs/check-results.mjs +28 -0
  77. package/runtime/scripts/hfs/check.mjs +46 -47
  78. package/runtime/scripts/hfs/coverage-from.mjs +70 -0
  79. package/runtime/scripts/hfs/coverage-scope.mjs +1 -1
  80. package/runtime/scripts/hfs/db-baseline-verb.mjs +276 -0
  81. package/runtime/scripts/hfs/db-plan-verb.mjs +254 -0
  82. package/runtime/scripts/hfs/db-restamp-verb.mjs +60 -0
  83. package/runtime/scripts/hfs/declaration-shape.mjs +39 -3
  84. package/runtime/scripts/hfs/doc-commands.mjs +367 -0
  85. package/runtime/scripts/hfs/edition-slots.mjs +2 -2
  86. package/runtime/scripts/hfs/example-secrets.mjs +158 -0
  87. package/runtime/scripts/hfs/manifest-shape.mjs +69 -9
  88. package/runtime/scripts/hfs/param-names.mjs +1 -1
  89. package/runtime/scripts/hfs/refusal-codes.mjs +6 -0
  90. package/runtime/scripts/hfs/rule-catalog.mjs +158 -11
  91. package/runtime/scripts/hfs/rule-params-shape.mjs +22 -10
  92. package/runtime/scripts/hfs/rules/a15-findings.mjs +88 -0
  93. package/runtime/scripts/hfs/rules/app-env-required.mjs +20 -0
  94. package/runtime/scripts/hfs/rules/app-root.mjs +2 -2
  95. package/runtime/scripts/hfs/rules/assets.mjs +79 -0
  96. package/runtime/scripts/hfs/rules/boot-proof.mjs +16 -0
  97. package/runtime/scripts/hfs/rules/canon-copy-drift.mjs +59 -0
  98. package/runtime/scripts/hfs/rules/cli.mjs +1 -1
  99. package/runtime/scripts/hfs/rules/contract-compat.mjs +1 -1
  100. package/runtime/scripts/hfs/rules/contract.mjs +67 -6
  101. package/runtime/scripts/hfs/rules/coverage-below.mjs +230 -0
  102. package/runtime/scripts/hfs/rules/database-access.mjs +343 -0
  103. package/runtime/scripts/hfs/rules/database-baseline.mjs +142 -0
  104. package/runtime/scripts/hfs/rules/database-config.mjs +250 -2
  105. package/runtime/scripts/hfs/rules/database-constants.mjs +9 -0
  106. package/runtime/scripts/hfs/rules/database-migrations.mjs +286 -7
  107. package/runtime/scripts/hfs/rules/database-plpgsql.mjs +119 -9
  108. package/runtime/scripts/hfs/rules/database-queue-sources.mjs +176 -0
  109. package/runtime/scripts/hfs/rules/database-queues.mjs +361 -0
  110. package/runtime/scripts/hfs/rules/database-sql.mjs +91 -9
  111. package/runtime/scripts/hfs/rules/database.mjs +9 -0
  112. package/runtime/scripts/hfs/rules/deploy-scan.mjs +192 -0
  113. package/runtime/scripts/hfs/rules/deploy-workflow.mjs +219 -0
  114. package/runtime/scripts/hfs/rules/doc-fresh.mjs +286 -0
  115. package/runtime/scripts/hfs/rules/docker-health.mjs +249 -0
  116. package/runtime/scripts/hfs/rules/docker.mjs +24 -7
  117. package/runtime/scripts/hfs/rules/edition.mjs +9 -8
  118. package/runtime/scripts/hfs/rules/env-parity.mjs +373 -0
  119. package/runtime/scripts/hfs/rules/env-value-shape.mjs +113 -0
  120. package/runtime/scripts/hfs/rules/event-bus.mjs +3 -2
  121. package/runtime/scripts/hfs/rules/event-tables.mjs +100 -0
  122. package/runtime/scripts/hfs/rules/fe-contract-documents.mjs +115 -15
  123. package/runtime/scripts/hfs/rules/fe-stack.mjs +57 -0
  124. package/runtime/scripts/hfs/rules/feature-files.mjs +28 -0
  125. package/runtime/scripts/hfs/rules/frontend-tree.mjs +54 -35
  126. package/runtime/scripts/hfs/rules/integration-specs.mjs +3 -2
  127. package/runtime/scripts/hfs/rules/lexicon.mjs +62 -0
  128. package/runtime/scripts/hfs/rules/lite-proof-lane.mjs +89 -0
  129. package/runtime/scripts/hfs/rules/monorepo.mjs +14 -1
  130. package/runtime/scripts/hfs/rules/pipeline.mjs +8 -1
  131. package/runtime/scripts/hfs/rules/release-provenance.mjs +211 -0
  132. package/runtime/scripts/hfs/rules/saga-delivery.mjs +66 -0
  133. package/runtime/scripts/hfs/rules/saga.mjs +8 -16
  134. package/runtime/scripts/hfs/rules/script-hygiene.mjs +151 -0
  135. package/runtime/scripts/hfs/rules/secret-custody-only.mjs +65 -0
  136. package/runtime/scripts/hfs/rules/secrets.mjs +13 -0
  137. package/runtime/scripts/hfs/rules/services.mjs +4 -4
  138. package/runtime/scripts/hfs/rules/stack-tooling.mjs +93 -0
  139. package/runtime/scripts/hfs/rules/stacks-deploy.mjs +76 -0
  140. package/runtime/scripts/hfs/rules/stacks-ingress.mjs +87 -0
  141. package/runtime/scripts/hfs/rules/stacks-lite-backing.mjs +111 -0
  142. package/runtime/scripts/hfs/rules/stacks-release.mjs +366 -0
  143. package/runtime/scripts/hfs/rules/stacks-runtime.mjs +41 -1
  144. package/runtime/scripts/hfs/rules/stacks-swarm.mjs +417 -0
  145. package/runtime/scripts/hfs/rules/supabase-secrets.mjs +2 -2
  146. package/runtime/scripts/hfs/rules/supply-chain.mjs +167 -0
  147. package/runtime/scripts/hfs/rules/unused-symbol-flow.mjs +101 -0
  148. package/runtime/scripts/hfs/rules/unused-symbol-ledger.mjs +77 -0
  149. package/runtime/scripts/hfs/rules/unused-symbol-translators.mjs +165 -0
  150. package/runtime/scripts/hfs/rules/unused-symbol-values.mjs +103 -0
  151. package/runtime/scripts/hfs/rules/unused-symbol.mjs +320 -0
  152. package/runtime/scripts/hfs/runtime-rules/source-ast.mjs +185 -0
  153. package/runtime/scripts/hfs/runtime-rules/workflow-source.mjs +19 -0
  154. package/runtime/scripts/hfs/sealed-members.mjs +101 -0
  155. package/runtime/scripts/hfs/secret-bootstrap-crypto.mjs +200 -0
  156. package/runtime/scripts/hfs/secret-edit.mjs +170 -0
  157. package/runtime/scripts/hfs/secret-init.mjs +127 -19
  158. package/runtime/scripts/hfs/secret-sops.mjs +38 -6
  159. package/runtime/scripts/hfs/secret-store.mjs +56 -54
  160. package/runtime/scripts/hfs/secret.mjs +34 -5
  161. package/runtime/scripts/hfs/slot-imports.mjs +18 -5
  162. package/runtime/scripts/hfs/slots.mjs +3 -2
  163. package/runtime/scripts/hfs/stack-config-kit.mjs +34 -0
  164. package/runtime/scripts/hfs/stack-deploy-config.mjs +313 -0
  165. package/runtime/scripts/hfs/stack-ingress-config.mjs +237 -0
  166. package/runtime/scripts/hfs/stacks-gate.mjs +414 -0
  167. package/runtime/scripts/hfs/starcistacks-runtime.mjs +61 -0
  168. package/runtime/scripts/hfs/typescript-programs.mjs +0 -8
  169. package/runtime/scripts/lib/command-tokens.mjs +126 -0
  170. package/runtime/scripts/lib/compose-values.mjs +11 -0
  171. package/runtime/scripts/lib/deployment-findings.mjs +7 -0
  172. package/runtime/scripts/lib/env.mjs +2 -2
  173. package/runtime/scripts/lib/git.mjs +1 -1
  174. package/runtime/scripts/lib/i18n.mjs +0 -2
  175. package/runtime/scripts/lib/json-clone.mjs +61 -0
  176. package/runtime/scripts/lib/language.mjs +1 -1
  177. package/runtime/scripts/lib/list.mjs +4 -0
  178. package/runtime/scripts/lib/roots.mjs +2 -4
  179. package/runtime/scripts/lib/runtime-example.mjs +13 -0
  180. package/runtime/scripts/lib/secret-patterns.mjs +60 -5
  181. package/runtime/scripts/lib/sql-ast.mjs +32 -0
  182. package/runtime/scripts/lib/stack-services.mjs +1 -1
  183. package/runtime/scripts/lib/swarm-stack.mjs +5 -18
  184. package/runtime/scripts/lib/trim-end.mjs +8 -0
  185. package/runtime/scripts/lib/ts-analysis.mjs +5 -2
  186. package/runtime/scripts/lib/ts-ast.mjs +27 -2
  187. package/runtime/scripts/lib/workflow-events.mjs +8 -0
  188. package/scaffold/add-app.mjs +77 -59
  189. package/scaffold/add-cli-lite.mjs +14 -6
  190. package/scaffold/add-compose.mjs +79 -0
  191. package/scaffold/add-database-lite.mjs +14 -0
  192. package/scaffold/add-fe-overlay.mjs +1 -0
  193. package/scaffold/add-fe-page.mjs +1 -0
  194. package/scaffold/add-inbox-lite.mjs +74 -42
  195. package/scaffold/add-table.mjs +110 -87
  196. package/scaffold/add.mjs +159 -44
  197. package/scaffold/app.mjs +124 -39
  198. package/scaffold/edition-gate.mjs +3 -3
  199. package/scaffold/lite-exports.mjs +2 -1
  200. package/scaffold/service.mjs +2 -2
  201. package/scaffold/supabase-ports.mjs +1 -1
  202. package/src/main.mjs +229 -116
  203. package/stack/age-install.mjs +37 -0
  204. package/stack/backup.mjs +100 -0
  205. package/stack/build-args.mjs +42 -0
  206. package/stack/build.mjs +173 -0
  207. package/stack/cli.mjs +130 -0
  208. package/stack/config.mjs +57 -0
  209. package/stack/context.mjs +57 -0
  210. package/stack/converge.mjs +107 -0
  211. package/stack/deploy.mjs +301 -0
  212. package/stack/digest.mjs +16 -0
  213. package/stack/docker-transport.mjs +53 -0
  214. package/stack/edge-alias.mjs +118 -0
  215. package/stack/errors.mjs +21 -0
  216. package/stack/image-scan.mjs +152 -0
  217. package/stack/ingress-create.mjs +114 -0
  218. package/stack/ingress-publish.mjs +273 -0
  219. package/stack/ingress.mjs +706 -0
  220. package/stack/jobs.mjs +51 -0
  221. package/stack/live-restore.mjs +177 -0
  222. package/stack/prepare.mjs +266 -0
  223. package/stack/private-file.mjs +22 -0
  224. package/stack/provision.mjs +90 -0
  225. package/stack/prune.mjs +46 -0
  226. package/stack/release-store.mjs +87 -0
  227. package/stack/remote.mjs +205 -0
  228. package/stack/render.mjs +149 -0
  229. package/stack/retire.mjs +47 -0
  230. package/stack/sealed.mjs +149 -0
  231. package/stack/task.mjs +42 -0
  232. package/stack/verify.mjs +99 -0
  233. package/sync/ci-commands.mjs +13 -0
  234. package/sync/format.mjs +1 -1
  235. package/sync/hygiene.mjs +1 -1
  236. package/sync/index.mjs +110 -54
  237. package/sync/stack-images.mjs +127 -0
  238. package/templates/app/ci-workflows/github/workflows/ci.yml +5 -1
  239. package/templates/app/ci-workflows/github/workflows/images.yml +15 -3
  240. package/templates/app/ci-workflows/github/workflows/stack-deploy.yml +148 -0
  241. package/templates/app/ci-workflows-lite/github/workflows/ci.yml +7 -3
  242. package/templates/app/ci-workflows-lite/github/workflows/db-deploy.yml +38 -6
  243. package/templates/app/ci-workflows-lite/github/workflows/images.yml +14 -2
  244. package/templates/app/ci-workflows-lite/github/workflows/stack-deploy.yml +148 -0
  245. package/templates/app/codegen-graphql.mjs.partial +13 -2
  246. package/templates/app/codegen-lite/scripts/codegen.mjs +6 -1
  247. package/templates/app/codegen-prelude.mjs.partial +8 -0
  248. package/templates/app/gitignore +3 -0
  249. package/templates/app/gitignore-lite +3 -0
  250. package/templates/app/package-scripts/package.json +1 -0
  251. package/templates/app/package-scripts/supabase.json +7 -0
  252. package/templates/app/package-scripts-lite/package.json +1 -7
  253. package/templates/app/skeleton/.editorconfig +1 -0
  254. package/templates/app/skeleton/.gitattributes +7 -1
  255. package/templates/app/skeleton/.github/dependabot.yml +17 -0
  256. package/templates/app/skeleton/.nvmrc +1 -1
  257. package/templates/app/skeleton/.starcistacks/application-stacks.yaml +134 -1
  258. package/templates/app/skeleton/.starcistacks/dev/README.md +16 -0
  259. package/templates/app/skeleton/.starcistacks/dev/environment.json +5 -0
  260. package/templates/app/skeleton/.starcistacks/dev/infra/compose/stack.yaml +71 -0
  261. package/templates/app/skeleton/.starcistacks/dev/runtime/env/KEYS.md +14 -0
  262. package/templates/app/skeleton/.starcistacks/production/README.md +37 -0
  263. package/templates/app/skeleton/.starcistacks/production/environment.json +5 -0
  264. package/templates/app/skeleton/.starcistacks/production/infra/swarm/Caddyfile +15 -0
  265. package/templates/app/skeleton/.starcistacks/production/infra/swarm/owner-actions.md +11 -0
  266. package/templates/app/skeleton/.starcistacks/production/infra/swarm/recovery/restore-proof.md +7 -0
  267. package/templates/app/skeleton/.starcistacks/production/infra/swarm/releases/current.yaml +6 -0
  268. package/templates/app/skeleton/.starcistacks/production/infra/swarm/releases/images.lock.yaml +9 -0
  269. package/templates/app/skeleton/.starcistacks/production/infra/swarm/releases/previous.yaml +6 -0
  270. package/templates/app/skeleton/.starcistacks/production/infra/swarm/stack.yaml +142 -0
  271. package/templates/app/skeleton/.starcistacks/production/runtime/env/KEYS.md +14 -0
  272. package/templates/app/skeleton/README.md +6 -6
  273. package/templates/app/skeleton/scripts/codegen.mjs +1 -2
  274. package/templates/app/skeleton-lite/.editorconfig +16 -0
  275. package/templates/app/skeleton-lite/.gitattributes +8 -0
  276. package/templates/app/skeleton-lite/.github/dependabot.yml +17 -0
  277. package/templates/app/skeleton-lite/.nvmrc +1 -0
  278. package/templates/app/skeleton-lite/.sops.yaml +3 -2
  279. package/templates/app/skeleton-lite/.starcistacks/application-stacks.yaml +178 -1
  280. package/templates/app/skeleton-lite/.starcistacks/dev/README.md +20 -0
  281. package/templates/app/skeleton-lite/.starcistacks/dev/environment.json +2 -1
  282. package/templates/app/skeleton-lite/.starcistacks/dev/infra/swarm/Caddyfile +16 -0
  283. package/templates/app/skeleton-lite/.starcistacks/dev/infra/swarm/owner-actions.md +11 -0
  284. package/templates/app/skeleton-lite/.starcistacks/dev/infra/swarm/recovery/restore-proof.md +7 -0
  285. package/templates/app/skeleton-lite/.starcistacks/dev/infra/swarm/releases/current.yaml +6 -0
  286. package/templates/app/skeleton-lite/.starcistacks/dev/infra/swarm/releases/images.lock.yaml +8 -0
  287. package/templates/app/skeleton-lite/.starcistacks/dev/infra/swarm/releases/previous.yaml +6 -0
  288. package/templates/app/skeleton-lite/.starcistacks/dev/infra/swarm/stack.yaml +120 -0
  289. package/templates/app/skeleton-lite/.starcistacks/dev/runtime/env/KEYS.md +18 -0
  290. package/templates/app/skeleton-lite/.starcistacks/production/README.md +38 -0
  291. package/templates/app/skeleton-lite/.starcistacks/production/environment.json +6 -0
  292. package/templates/app/skeleton-lite/.starcistacks/production/infra/swarm/Caddyfile +11 -0
  293. package/templates/app/skeleton-lite/.starcistacks/production/infra/swarm/owner-actions.md +11 -0
  294. package/templates/app/skeleton-lite/.starcistacks/production/infra/swarm/recovery/restore-proof.md +7 -0
  295. package/templates/app/skeleton-lite/.starcistacks/production/infra/swarm/releases/current.yaml +6 -0
  296. package/templates/app/skeleton-lite/.starcistacks/production/infra/swarm/releases/images.lock.yaml +8 -0
  297. package/templates/app/skeleton-lite/.starcistacks/production/infra/swarm/releases/previous.yaml +6 -0
  298. package/templates/app/skeleton-lite/.starcistacks/production/infra/swarm/stack.yaml +121 -0
  299. package/templates/app/skeleton-lite/.starcistacks/production/runtime/env/KEYS.md +21 -0
  300. package/templates/app/skeleton-lite/.starciwork/brand/index.yaml +12 -0
  301. package/templates/app/skeleton-lite/README.md +5 -3
  302. package/templates/app/skeleton-lite/supabase/config.toml +8 -1
  303. package/templates/app/skeleton-lite/supabase/migrations/__timestamp___baseline.sql +15 -6
  304. package/templates/app/upgrade-full/.starcistacks/dev/README.md +13 -7
  305. package/templates/be/image/worker/Dockerfile +1 -1
  306. package/templates/be/inbox/database-client.spec.ts.tpl +17 -18
  307. package/templates/be/inbox/database-client.ts.tpl +12 -11
  308. package/templates/be/inbox/delivery-recorded.event.ts.tpl +36 -0
  309. package/templates/be/inbox/event-index.ts.tpl +1 -0
  310. package/templates/be/inbox/events.json.tpl +10 -0
  311. package/templates/be/inbox/migration.sql.tpl +1 -0
  312. package/templates/be/inbox/platform/event-bus/event-bus.contracts.ts.tpl +42 -0
  313. package/templates/be/inbox/platform/event-bus/event-bus.decorators.ts.tpl +9 -0
  314. package/templates/be/inbox/platform/event-bus/event-bus.module-definition.ts.tpl +6 -0
  315. package/templates/be/inbox/platform/event-bus/event-bus.module.ts.tpl +19 -0
  316. package/templates/be/inbox/platform/event-bus/event-bus.options.ts.tpl +2 -0
  317. package/templates/be/inbox/platform/event-bus/event-bus.port.ts.tpl +16 -0
  318. package/templates/be/inbox/platform/event-bus/event-bus.service.spec.ts.tpl +45 -0
  319. package/templates/be/inbox/platform/event-bus/event-bus.service.ts.tpl +15 -0
  320. package/templates/be/inbox/platform/event-bus/index.ts.tpl +5 -0
  321. package/templates/be/inbox/platform/event-bus/persistence/event-bus.sql.ts.tpl +5 -0
  322. package/templates/be/inbox/platform/inbox/inbox.decorators.ts.tpl +9 -0
  323. package/templates/be/inbox/platform/inbox/inbox.module-definition.ts.tpl +6 -0
  324. package/templates/be/inbox/platform/inbox/inbox.module.ts.tpl +19 -0
  325. package/templates/be/inbox/platform/inbox/inbox.options.ts.tpl +2 -0
  326. package/templates/be/inbox/platform/inbox/inbox.port.ts.tpl +10 -0
  327. package/templates/be/inbox/platform/inbox/inbox.service.spec.ts.tpl +41 -0
  328. package/templates/be/inbox/platform/inbox/inbox.service.ts.tpl +19 -0
  329. package/templates/be/inbox/platform/inbox/index.ts.tpl +3 -0
  330. package/templates/be/inbox/platform/inbox/persistence/inbox.sql.ts.tpl +7 -0
  331. package/templates/be/inbox/platform/migration.sql.tpl +48 -0
  332. package/templates/be/inbox/service.spec.ts.tpl +42 -9
  333. package/templates/be/inbox/service.ts.tpl +23 -8
  334. package/templates/be/inbox/webhook-door.spec.ts.tpl +1 -1
  335. package/templates/be/inbox/webhook-door.ts.tpl +2 -2
  336. package/templates/be/patterns/api/feature.module-definition.ts.tpl +6 -0
  337. package/templates/be/patterns/api/feature.module.ts.tpl +2 -1
  338. package/templates/be/patterns/api/feature.options.ts.tpl +2 -0
  339. package/templates/be/patterns/api/graphql.module-definition.ts.tpl +6 -0
  340. package/templates/be/patterns/api/graphql.module.ts.tpl +3 -2
  341. package/templates/be/patterns/api/graphql.options.ts.tpl +2 -0
  342. package/templates/be/patterns/cli/group.module-definition.ts.tpl +6 -0
  343. package/templates/be/patterns/cli/group.module.ts.tpl +2 -1
  344. package/templates/be/patterns/cli/group.options.ts.tpl +2 -0
  345. package/templates/be/patterns/event-bus/platform/errors/event-bus.error.ts.tpl +0 -3
  346. package/templates/be/patterns/event-bus/platform/messages/event-bus.messages.ts.tpl +0 -2
  347. package/templates/be/patterns/jobs/job.module-definition.ts.tpl +6 -0
  348. package/templates/be/patterns/jobs/job.module.ts.tpl +2 -1
  349. package/templates/be/patterns/jobs/job.options.ts.tpl +2 -0
  350. package/templates/be/patterns/jobs/job.queue.module-definition.ts.tpl +6 -0
  351. package/templates/be/patterns/jobs/job.queue.module.ts.tpl +6 -3
  352. package/templates/be/patterns/jobs/job.queue.options.ts.tpl +2 -0
  353. package/templates/be/patterns/jobs/platform/fenced.processor.spec.ts.tpl +3 -1
  354. package/templates/be/patterns/outbox/platform/outbox-relay.policy.spec.ts.tpl +25 -9
  355. package/templates/be/patterns/queues/platform/queue.config.ts.tpl +1 -1
  356. package/templates/be/patterns/queues/platform/queue.index.ts.tpl +1 -1
  357. package/templates/be/patterns/queues/platform/queue.module-definition.ts.tpl +2 -2
  358. package/templates/be/patterns/queues/platform/queue.module.ts.tpl +11 -0
  359. package/templates/be/patterns/queues/platform/queue.options.ts.tpl +10 -0
  360. package/templates/be/patterns/queues/queue.module-definition.ts.tpl +6 -0
  361. package/templates/be/patterns/queues/queue.module.ts.tpl +2 -1
  362. package/templates/be/patterns/queues/queue.options.ts.tpl +2 -0
  363. package/templates/be/patterns/reactors/message.module-definition.ts.tpl +6 -0
  364. package/templates/be/patterns/reactors/message.module.ts.tpl +6 -3
  365. package/templates/be/patterns/reactors/message.options.ts.tpl +2 -0
  366. package/templates/be/patterns/reactors/reactor.module-definition.ts.tpl +6 -0
  367. package/templates/be/patterns/reactors/reactor.module.ts.tpl +2 -1
  368. package/templates/be/patterns/reactors/reactor.options.ts.tpl +2 -0
  369. package/templates/be/patterns/realtime/channel.gateway.spec.ts.tpl +188 -0
  370. package/templates/be/patterns/realtime/channel.gateway.ts.tpl +22 -0
  371. package/templates/be/patterns/realtime/channel.graphql.module-definition.ts.tpl +6 -0
  372. package/templates/be/patterns/realtime/channel.graphql.module.ts.tpl +2 -1
  373. package/templates/be/patterns/realtime/channel.graphql.options.ts.tpl +2 -0
  374. package/templates/be/patterns/realtime/channel.websocket.module-definition.ts.tpl +6 -0
  375. package/templates/be/patterns/realtime/channel.websocket.module.ts.tpl +110 -0
  376. package/templates/be/patterns/realtime/channel.websocket.options.ts.tpl +7 -0
  377. package/templates/be/patterns/saga/saga.message.module-definition.ts.tpl +6 -0
  378. package/templates/be/patterns/saga/saga.message.module.ts.tpl +6 -3
  379. package/templates/be/patterns/saga/saga.message.options.ts.tpl +2 -0
  380. package/templates/be/patterns/saga/saga.module-definition.ts.tpl +6 -0
  381. package/templates/be/patterns/saga/saga.module.ts.tpl +2 -1
  382. package/templates/be/patterns/saga/saga.options.ts.tpl +2 -0
  383. package/templates/be/patterns/webhooks/webhook.http.module-definition.ts.tpl +6 -0
  384. package/templates/be/patterns/webhooks/webhook.http.module.ts.tpl +2 -1
  385. package/templates/be/patterns/webhooks/webhook.http.options.ts.tpl +2 -0
  386. package/templates/be/platform/graphql/localize-errors.mapper.spec.ts +80 -26
  387. package/templates/be/platform/graphql/localize-errors.mapper.ts +2 -5
  388. package/templates/be/skeleton/apps/__app__/src/app.module.ts +11 -9
  389. package/templates/be/skeleton/apps/__app__/src/compose/api.ts +4 -0
  390. package/templates/be/skeleton/apps/__app__/src/compose/domain.ts +11 -0
  391. package/templates/be/skeleton/apps/__app__/src/compose/platform.ts +2 -0
  392. package/templates/be/skeleton/apps/__app__/src/compose/system.ts +5 -0
  393. package/templates/be/skeleton/apps/__app__/src/compose/webhooks.ts +4 -0
  394. package/templates/be/skeleton/apps/__app__/src/main.ts +15 -1
  395. package/templates/be/skeleton/apps/cli/src/app.module.ts +3 -13
  396. package/templates/be/skeleton/apps/cli/src/cli.options.ts +6 -14
  397. package/templates/be/skeleton/apps/cli/src/compose/cli.ts +5 -0
  398. package/templates/be/skeleton/apps/cli/src/compose/platform.ts +24 -0
  399. package/templates/be/skeleton/src/features/api/system-health/application/check-readiness.contracts.ts +2 -0
  400. package/templates/be/skeleton/src/features/api/system-health/application/check-readiness.handler.ts +22 -0
  401. package/templates/be/skeleton/src/features/api/system-health/application/check-readiness.query.ts +11 -0
  402. package/templates/be/skeleton/src/features/api/system-health/system-health.module-definition.ts +6 -0
  403. package/templates/be/skeleton/src/features/api/system-health/system-health.module.ts +5 -3
  404. package/templates/be/skeleton/src/features/api/system-health/system-health.options.ts +2 -0
  405. package/templates/be/skeleton/src/features/api/system-health/transport/http/ready.controller.ts +19 -0
  406. package/templates/be/skeleton/src/features/api/system-health/transport/http/system-health-http.module-definition.ts +6 -0
  407. package/templates/be/skeleton/src/features/api/system-health/transport/http/system-health-http.module.ts +8 -3
  408. package/templates/be/skeleton/src/features/api/system-health/transport/http/system-health-http.options.ts +2 -0
  409. package/templates/be/skeleton/src/features/cli/cli.module-definition.ts +6 -0
  410. package/templates/be/skeleton/src/features/cli/cli.module.ts +3 -2
  411. package/templates/be/skeleton/src/features/cli/cli.options.ts +2 -0
  412. package/templates/be/skeleton/src/features/cli/migrate/migrate.module-definition.ts +6 -0
  413. package/templates/be/skeleton/src/features/cli/migrate/migrate.module.ts +2 -1
  414. package/templates/be/skeleton/src/features/cli/migrate/migrate.options.ts +2 -0
  415. package/templates/be/skeleton/src/features/cli/seed/seed.module-definition.ts +6 -0
  416. package/templates/be/skeleton/src/features/cli/seed/seed.module.ts +2 -1
  417. package/templates/be/skeleton/src/features/cli/seed/seed.options.ts +2 -0
  418. package/templates/be/skeleton/src/features/cli/seed/subs/run.cli.spec.ts +5 -6
  419. package/templates/be/skeleton/src/modules/domain/identity/admission.policy.spec.ts +56 -4
  420. package/templates/be/skeleton/src/modules/domain/identity/admission.policy.ts +36 -5
  421. package/templates/be/skeleton/src/modules/domain/identity/auth.guard.spec.ts +39 -8
  422. package/templates/be/skeleton/src/modules/domain/identity/auth.guard.ts +14 -8
  423. package/templates/be/skeleton/src/modules/domain/identity/credential.contracts.ts +26 -0
  424. package/templates/be/skeleton/src/modules/domain/identity/credential.guard.spec.ts +119 -0
  425. package/templates/be/skeleton/src/modules/domain/identity/credential.guard.ts +43 -0
  426. package/templates/be/skeleton/src/modules/domain/identity/identity.contracts.ts +16 -0
  427. package/templates/be/skeleton/src/modules/domain/identity/identity.decorators.ts +18 -1
  428. package/templates/be/skeleton/src/modules/domain/identity/identity.module.ts +22 -4
  429. package/templates/be/skeleton/src/modules/domain/identity/identity.options.ts +8 -2
  430. package/templates/be/skeleton/src/modules/domain/identity/index.ts +1 -0
  431. package/templates/be/skeleton/src/modules/platform/database/connection-source.client.spec.ts +11 -4
  432. package/templates/be/skeleton/src/modules/platform/database/database-probe.service.spec.ts +32 -0
  433. package/templates/be/skeleton/src/modules/platform/database/database-probe.service.ts +14 -0
  434. package/templates/be/skeleton/src/modules/platform/database/database.module.ts +7 -1
  435. package/templates/be/skeleton/src/modules/platform/database/index.ts +2 -0
  436. package/templates/be/skeleton/src/modules/platform/database/primary.decorators.ts +8 -0
  437. package/templates/be/skeleton/src/modules/platform/database/seed-connections.client.spec.ts +25 -6
  438. package/templates/be/skeleton/src/modules/platform/errors/errors.filter.spec.ts +16 -6
  439. package/templates/be/skeleton/src/modules/platform/http-security/edge-security.service.spec.ts +130 -0
  440. package/templates/be/skeleton/src/modules/platform/http-security/edge-security.service.ts +58 -0
  441. package/templates/be/skeleton/src/modules/platform/http-security/errors/http-security.error.ts +3 -0
  442. package/templates/be/skeleton/src/modules/platform/http-security/execution-request.mapper.spec.ts +16 -4
  443. package/templates/be/skeleton/src/modules/platform/http-security/http-security.config.ts +16 -10
  444. package/templates/be/skeleton/src/modules/platform/http-security/http-security.module.ts +3 -1
  445. package/templates/be/skeleton/src/modules/platform/http-security/http-security.options.ts +2 -0
  446. package/templates/be/skeleton/src/modules/platform/http-security/index.ts +5 -0
  447. package/templates/be/skeleton/src/modules/platform/http-security/messages/http-security.messages.ts +12 -5
  448. package/templates/be/skeleton/src/modules/platform/http-security/rate-limit.guard.spec.ts +13 -8
  449. package/templates/be/skeleton/src/modules/platform/http-security/readiness.contracts.ts +5 -0
  450. package/templates/be/skeleton/src/modules/platform/http-security/readiness.decorators.ts +7 -0
  451. package/templates/be/skeleton/src/modules/platform/http-security/readiness.service.spec.ts +86 -0
  452. package/templates/be/skeleton/src/modules/platform/http-security/readiness.service.ts +39 -0
  453. package/templates/be/skeleton/src/modules/platform/http-security/webhook-signature.service.spec.ts +139 -131
  454. package/templates/be/skeleton/src/modules/platform/i18n/index.ts +1 -1
  455. package/templates/be/skeleton/src/modules/platform/logging/json-logger.service.spec.ts +91 -67
  456. package/templates/be/skeleton/src/modules/platform/logging/json-logger.service.ts +5 -4
  457. package/templates/be/skeleton/src/modules/platform/logging/logging.log-events.ts +0 -2
  458. package/templates/be/skeleton/src/modules/platform/primitives/outcome.mapper.spec.ts +20 -6
  459. package/templates/be/skeleton/src/modules/platform/primitives/sequence.policy.spec.ts +16 -4
  460. package/templates/be/skeleton/src/modules/platform/retry/index.ts +3 -0
  461. package/templates/be/skeleton/src/modules/platform/retry/retry.decorators.ts +9 -0
  462. package/templates/be/skeleton/src/modules/platform/retry/retry.module-definition.ts +7 -0
  463. package/templates/be/skeleton/src/modules/platform/retry/retry.module.ts +19 -0
  464. package/templates/be/skeleton/src/modules/platform/retry/retry.options.ts +2 -0
  465. package/templates/be/skeleton/src/modules/platform/retry/retry.port.ts +11 -0
  466. package/templates/be/skeleton/src/modules/platform/retry/system-timer.service.spec.ts +36 -0
  467. package/templates/be/skeleton/src/modules/platform/retry/system-timer.service.ts +10 -0
  468. package/templates/be/skeleton/src/tests/contract/http-security/http-security.contract-spec.ts +54 -0
  469. package/templates/be/skeleton-lite/apps/api/src/app.module.ts +14 -18
  470. package/templates/be/skeleton-lite/apps/api/src/compose/api.ts +4 -0
  471. package/templates/be/skeleton-lite/apps/api/src/compose/domain.ts +10 -0
  472. package/templates/be/skeleton-lite/apps/api/src/compose/integrations.ts +16 -0
  473. package/templates/be/skeleton-lite/apps/api/src/compose/platform.ts +2 -0
  474. package/templates/be/skeleton-lite/apps/api/src/compose/system.ts +5 -0
  475. package/templates/be/skeleton-lite/apps/api/src/compose/webhooks.ts +4 -0
  476. package/templates/be/skeleton-lite/apps/api/src/main.ts +2 -3
  477. package/templates/be/skeleton-lite/apps/cli/src/compose/platform.ts +12 -0
  478. package/templates/be/skeleton-lite/src/modules/domain/identity/admission.policy.spec.ts +71 -5
  479. package/templates/be/skeleton-lite/src/modules/domain/identity/admission.policy.ts +29 -2
  480. package/templates/be/skeleton-lite/src/modules/domain/identity/auth.guard.spec.ts +68 -15
  481. package/templates/be/skeleton-lite/src/modules/domain/identity/auth.guard.ts +14 -9
  482. package/templates/be/skeleton-lite/src/modules/domain/identity/identity.contracts.ts +9 -2
  483. package/templates/be/skeleton-lite/src/modules/domain/identity/identity.decorators.ts +16 -1
  484. package/templates/be/skeleton-lite/src/modules/domain/identity/identity.module.ts +19 -4
  485. package/templates/be/skeleton-lite/src/modules/domain/identity/identity.options.ts +5 -1
  486. package/templates/be/skeleton-lite/src/modules/domain/identity/index.ts +2 -1
  487. package/templates/be/skeleton-lite/src/modules/integrations/supabase/errors/supabase.error.ts +0 -3
  488. package/templates/be/skeleton-lite/src/modules/integrations/supabase/supabase.client.spec.ts +17 -5
  489. package/templates/be/skeleton-lite/src/modules/platform/database/database.module.ts +24 -13
  490. package/templates/be/skeleton-lite/src/modules/platform/database/database.port.ts +5 -0
  491. package/templates/be/skeleton-lite/src/modules/platform/database/errors/database.error.ts +16 -0
  492. package/templates/be/skeleton-lite/src/modules/platform/database/index.ts +3 -0
  493. package/templates/be/skeleton-lite/src/modules/platform/http-security/http-security.module.ts +5 -2
  494. package/templates/be/skeleton-lite/src/modules/platform/http-security/index.ts +4 -1
  495. package/templates/be/skeleton-lite/src/modules/platform/i18n/index.ts +1 -1
  496. package/templates/be/skeleton-lite/src/modules/platform/logging/logging.log-events.ts +7 -0
  497. package/templates/be/skeleton-lite/src/tests/fixtures/builders/api.builder.ts +29 -0
  498. package/templates/be/skeleton-lite/src/tests/fixtures/builders/cli.builder.ts +16 -0
  499. package/templates/be/table/api/feature.module-definition.ts.tpl +6 -0
  500. package/templates/be/table/api/feature.module.ts.tpl +2 -1
  501. package/templates/be/table/api/feature.options.ts.tpl +2 -0
  502. package/templates/be/table/api/graphql.module-definition.ts.tpl +6 -0
  503. package/templates/be/table/api/graphql.module.ts.tpl +3 -2
  504. package/templates/be/table/api/graphql.options.ts.tpl +2 -0
  505. package/templates/be/table/migration.sql.tpl +1 -0
  506. package/templates/be/upgrade-full/apps/cli/src/app.module.ts +4 -6
  507. package/templates/be/upgrade-full/apps/cli/src/cli.env.ts +7 -0
  508. package/templates/be/upgrade-full/apps/cli/src/compose/cli.ts +5 -0
  509. package/templates/be/upgrade-full/apps/cli/src/compose/platform.ts +9 -0
  510. package/templates/be/upgrade-full/apps/cli/src/main.ts +3 -0
  511. package/templates/be/upgrade-full/src/features/cli/cli.module.ts +3 -2
  512. package/templates/fe/auth/supabase-client.ts.tpl +34 -0
  513. package/templates/fe/skeleton/apps/__app__/src/app/auth/callback/route.ts +11 -0
  514. package/templates/fe/skeleton/apps/__app__/src/components/blocks/SignInForm/index.tsx +6 -1
  515. package/templates/fe/skeleton/apps/__app__/src/components/leaves/Icon/index.tsx +4 -0
  516. package/templates/fe/skeleton/apps/__app__/src/features/overlays/AppGuideOverlay/component.tsx +31 -0
  517. package/templates/fe/skeleton/apps/__app__/src/features/overlays/AppGuideOverlay/index.tsx +24 -0
  518. package/templates/fe/skeleton/apps/__app__/src/hooks/auth/useSession.ts +6 -1
  519. package/templates/fe/skeleton/apps/__app__/src/hooks/auth/useSignOut.ts +3 -4
  520. package/templates/fe/skeleton/apps/__app__/src/hooks/zustand/appGuide/index.ts +1 -0
  521. package/templates/fe/skeleton/apps/__app__/src/hooks/zustand/appGuide/store.ts +15 -0
  522. package/templates/fe/skeleton/apps/__app__/src/modules/auth/pkce-client.ts +18 -0
  523. package/templates/fe/skeleton/apps/__app__/src/modules/auth/server.ts +19 -0
  524. package/templates/fe/skeleton/apps/app/src/features/pages/AppHomePage/index.tsx +7 -1
  525. package/templates/fe/skeleton/apps/app/src/modules/i18n/messages/en.json +57 -0
  526. package/templates/fe/skeleton/apps/app/src/modules/i18n/messages/vi.json +6 -0
  527. package/templates/fe/skeleton/apps/landing/src/modules/i18n/messages/en.json +31 -0
  528. package/templates/fe/skeleton/packages/__project__-i18n/src/routing.ts +1 -1
  529. package/templates/fe/skeleton/packages/__project__-ui/src/index.ts +1 -0
  530. package/templates/fe/skeleton-lite/apps/__app__/src/modules/auth/client.ts +1 -34
  531. package/templates/fe/skeleton-lite/apps/web/src/features/pages/AppHomePage/index.tsx +7 -1
  532. package/templates/fe/skeleton-lite/apps/web/src/modules/i18n/messages/en.json +57 -0
  533. package/templates/fe/skeleton-lite/apps/web/src/modules/i18n/messages/vi.json +30 -24
  534. package/templates/fe/skeleton-lite/apps/web/src/modules/i18n/routing.ts +1 -1
  535. package/templates/fe/transport/graphql/clients/clients/auth.ts +1 -1
  536. package/templates/fe/transport/graphql/clients/links/locale.ts +2 -2
  537. package/upgrade/check.mjs +3 -1
  538. package/upgrade/index.mjs +54 -41
@@ -1,8 +1,15 @@
1
1
  // database-config.mjs - L08 Supabase TOML policy and L09 generated-type drift checks. Parsing and type emission stay
2
2
  // lazy/injected so applications without Supabase do not load either tool and this rule starts no Docker process.
3
3
  import { sameText } from '../../lib/same-text.mjs';
4
- import { found, readText } from './read.mjs';
5
- import { DB_CONFIG_POLICY, DB_TYPES_DRIFT, TYPES_FILE } from './database-constants.mjs';
4
+ import path from 'node:path';
5
+ import { parse as parseTomlSync } from 'smol-toml';
6
+ import { found, readJson, readText } from './read.mjs';
7
+ import {
8
+ CONFIG_FILE, DB_API_EXPOSED, DB_CONFIG_POLICY, DB_EXTENSION_ALLOWED, DB_TYPES_DRIFT, DB_VAULT_WRITE,
9
+ HFS_SEED_PII, isActiveMigration, TYPES_FILE,
10
+ } from './database-constants.mjs';
11
+ import { databaseExecutableTokens, databaseSqlStatements, databaseSqlTokens, destructiveMigrationFindings } from './database-migrations.mjs';
12
+ import { byCodeUnit } from '../../lib/list.mjs';
6
13
  const ENV_REF = /^env\([A-Za-z_]\w*\)$/;
7
14
  const PUBLIC_KEYS = new Set(['anon_key', 'publishable_key', 'public_key']);
8
15
 
@@ -125,3 +132,244 @@ export async function typesFindings({ repoRoot, files, emitTypes, declaration })
125
132
  if (!sameText(committed, String(emitted))) return [found(DB_TYPES_DRIFT, TYPES_FILE, `${TYPES_FILE} differs from what \`contract:emit\` generates from the migrations now; run \`npm run contract:emit\` and commit the result`, { drift: 'stale' })];
126
133
  return [];
127
134
  }
135
+
136
+ const DEFAULT_EXTENSIONS = ['pgmq', 'pg_cron', 'pgcrypto', 'pg_trgm', 'vector', 'uuid-ossp', 'pg_net', 'supabase_vault', 'unaccent'];
137
+ const DEFAULT_API_SCHEMAS = ['public', 'graphql_public'];
138
+ const FORBIDDEN_API_SCHEMAS = new Set(['pgmq', 'pgmq_public']);
139
+ const word = (token, value) => token?.t === 'word' && token.v === value;
140
+ const identifier = (token) => token && ['word', 'ident'].includes(token.t) ? token.v : null;
141
+
142
+ function dbParameters(manifest) {
143
+ const declared = manifest?.ruleParams?.db ?? {};
144
+ return {
145
+ extensions: Array.isArray(declared.extensions) ? declared.extensions : DEFAULT_EXTENSIONS,
146
+ apiSchemas: Array.isArray(declared.apiSchemas) ? declared.apiSchemas : DEFAULT_API_SCHEMAS,
147
+ majorVersion: declared.majorVersion ?? 17,
148
+ };
149
+ }
150
+
151
+ function qualifiedName(tokens, index) {
152
+ const name = identifier(tokens[index]);
153
+ if (name === null) return null;
154
+ return tokens[index + 1]?.v === '.' && identifier(tokens[index + 2]) !== null
155
+ ? { schema: name, name: identifier(tokens[index + 2]), next: index + 3 }
156
+ : { schema: 'public', name, next: index + 1 };
157
+ }
158
+
159
+ function extensionFindings(file, tokens, allowed) {
160
+ const findings = [];
161
+ for (let index = 0; index < tokens.length; index += 1) {
162
+ if (!word(tokens[index], 'create') || !word(tokens[index + 1], 'extension')) continue;
163
+ let at = index + 2;
164
+ if (word(tokens[at], 'if') && word(tokens[at + 1], 'not') && word(tokens[at + 2], 'exists')) at += 3;
165
+ const name = identifier(tokens[at]);
166
+ if (name !== null && !allowed.has(name)) findings.push(found(DB_EXTENSION_ALLOWED, file,
167
+ `${file}:${tokens[index].line} enables extension ${name}, which ruleParams.db.extensions does not allow`, { line: tokens[index].line, extension: name }));
168
+ }
169
+ return findings;
170
+ }
171
+
172
+ function vaultWriteAt(tokens, index) {
173
+ const token = tokens[index];
174
+ if (identifier(token) === 'vault' && tokens[index + 1]?.v === '.' && tokens[index + 3]?.v === '(') {
175
+ return ['create_secret', 'update_secret', 'delete_secret'].includes(identifier(tokens[index + 2]));
176
+ }
177
+ let at = null;
178
+ if (word(token, 'insert') || word(token, 'merge')) at = word(tokens[index + 1], 'into') ? index + 2 : null;
179
+ if (word(token, 'delete')) at = word(tokens[index + 1], 'from') ? index + 2 : null;
180
+ if (word(token, 'update')) at = index + 1;
181
+ if (word(token, 'truncate')) return truncatedVault(tokens, index);
182
+ if (at === null) return false;
183
+ if (word(tokens[at], 'only')) at += 1;
184
+ return qualifiedName(tokens, at)?.schema === 'vault';
185
+ }
186
+
187
+ function truncatedVault(tokens, index) {
188
+ let at = word(tokens[index + 1], 'table') ? index + 2 : index + 1;
189
+ while (at < tokens.length) {
190
+ if (word(tokens[at], 'only')) at += 1;
191
+ const name = qualifiedName(tokens, at);
192
+ if (!name) return false;
193
+ if (name.schema === 'vault') return true;
194
+ if (tokens[name.next]?.v !== ',') return false;
195
+ at = name.next + 1;
196
+ }
197
+ return false;
198
+ }
199
+
200
+ function passwordStatement(statement) {
201
+ const starts = statement.findIndex((token, index) => (word(token, 'create') || word(token, 'alter'))
202
+ && (word(statement[index + 1], 'role') || word(statement[index + 1], 'user')));
203
+ return starts !== -1 && statement.slice(starts + 2).some((token) => word(token, 'password')) ? statement[starts] : null;
204
+ }
205
+
206
+ function custodySqlFindings(file, tokens) {
207
+ const findings = [];
208
+ for (const statement of databaseSqlStatements(tokens)) {
209
+ const password = passwordStatement(statement);
210
+ if (password) findings.push(found(DB_VAULT_WRITE, file,
211
+ `${file}:${password.line} sets a role password in a migration; provision credentials through sealed secret custody`, { line: password.line, operation: 'role-password' }));
212
+ const vault = statement.find((token, index) => vaultWriteAt(statement, index));
213
+ if (vault) findings.push(found(DB_VAULT_WRITE, file,
214
+ `${file}:${vault.line} writes to vault; migration SQL never provisions secret values`, { line: vault.line, operation: 'vault-write' }));
215
+ }
216
+ return findings;
217
+ }
218
+
219
+ function policyTarget(statement, start) {
220
+ const on = statement.findIndex((token, index) => index > start && word(token, 'on'));
221
+ return on === -1 ? null : qualifiedName(statement, on + 1);
222
+ }
223
+
224
+ function policyRoles(statement) {
225
+ const to = statement.findIndex((token) => word(token, 'to'));
226
+ if (to === -1) return null;
227
+ const end = statement.findIndex((token, index) => index > to && (word(token, 'using') || word(token, 'with')));
228
+ return statement.slice(to + 1, end === -1 ? undefined : end).map(identifier).filter(Boolean);
229
+ }
230
+
231
+ function applyPublicReadPolicy(statement, policies) {
232
+ const at = statement.findIndex((token, index) => ['create', 'alter', 'drop'].some((verb) => word(token, verb)) && word(statement[index + 1], 'policy'));
233
+ if (at === -1) return;
234
+ const verb = statement[at].v;
235
+ let nameAt = at + 2;
236
+ if (word(statement[nameAt], 'if') && word(statement[nameAt + 1], 'exists')) nameAt += 2;
237
+ const name = identifier(statement[nameAt]);
238
+ const target = policyTarget(statement, nameAt);
239
+ if (!name || !target) return;
240
+ const key = `${target.schema}.${target.name}.${name}`;
241
+ if (verb === 'drop') { policies.delete(key); return; }
242
+ const rename = statement.findIndex((token) => word(token, 'rename'));
243
+ if (verb === 'alter' && word(statement[rename + 1], 'to')) {
244
+ const nextName = identifier(statement[rename + 2]);
245
+ if (nextName && policies.has(key)) {
246
+ policies.set(`${target.schema}.${target.name}.${nextName}`, { ...policies.get(key), name: nextName });
247
+ policies.delete(key);
248
+ }
249
+ return;
250
+ }
251
+ const roles = policyRoles(statement);
252
+ if (verb === 'alter') {
253
+ if (policies.has(key) && roles !== null) policies.set(key, { ...policies.get(key), roles });
254
+ return;
255
+ }
256
+ const forAt = statement.findIndex((token) => word(token, 'for'));
257
+ policies.set(key, { name, schema: target.schema, select: word(statement[forAt + 1], 'select'), roles: roles ?? ['public'] });
258
+ }
259
+
260
+ function publicReadSchemas(sources) {
261
+ const policies = new Map();
262
+ for (const { tokens } of sources) for (const statement of databaseSqlStatements(tokens)) applyPublicReadPolicy(statement, policies);
263
+ return new Set([...policies.values()].filter((policy) => policy.select && policy.name.endsWith('_public_read')
264
+ && policy.roles.some((role) => ['anon', 'public'].includes(role))).map((policy) => policy.schema));
265
+ }
266
+
267
+ function schemaConfigFindings(file, config, parameters, edition, publicSchemas) {
268
+ const schemas = config.api?.schemas;
269
+ const findings = [];
270
+ const allowed = new Set(parameters.apiSchemas);
271
+ if (!Array.isArray(schemas) || schemas.some((schema) => typeof schema !== 'string' || !allowed.has(schema))) {
272
+ findings.push(found(DB_CONFIG_POLICY, file, `${file} api.schemas must be an array drawn from ruleParams.db.apiSchemas`, { key: 'api.schemas' }));
273
+ }
274
+ if (Array.isArray(schemas) && schemas.some((schema) => FORBIDDEN_API_SCHEMAS.has(schema))) findings.push(found(DB_API_EXPOSED, file,
275
+ `${file} api.schemas exposes a pgmq schema; queues are accessible only to the application role`, { key: 'api.schemas' }));
276
+ const exposed = Array.isArray(schemas) ? schemas : DEFAULT_API_SCHEMAS;
277
+ if (edition === 'lite' && config.api?.enabled !== false && exposed.length > 0 && !exposed.some((schema) => publicSchemas.has(schema))) {
278
+ findings.push(found(DB_API_EXPOSED, file, `${file} enables the lite data API without an exposed *_public_read FOR SELECT policy; disable api or set schemas = []`, { key: 'api.enabled' }));
279
+ }
280
+ return findings;
281
+ }
282
+
283
+ function safetyConfigFindings(file, config, parameters, supabase) {
284
+ const findings = [];
285
+ const anonymous = config.auth?.enable_anonymous_sign_ins;
286
+ if (anonymous !== undefined && (typeof anonymous !== 'boolean' || (anonymous && supabase?.enableAnonymousSignIns !== true))) findings.push(found(DB_CONFIG_POLICY, file,
287
+ `${file} auth.enable_anonymous_sign_ins is enabled without hfs.json supabase.enableAnonymousSignIns`, { key: 'auth.enable_anonymous_sign_ins' }));
288
+ if (config.edge_runtime?.enabled !== false) findings.push(found(DB_CONFIG_POLICY, file,
289
+ `${file} edge_runtime.enabled must be false; application execution belongs to the back end`, { key: 'edge_runtime.enabled' }));
290
+ const storageLimit = config.storage?.file_size_limit;
291
+ if (config.storage?.enabled !== false && (typeof storageLimit !== 'string' || !/^[1-9]\d*(?:B|KB|MB|GB|KiB|MiB|GiB)$/.test(storageLimit))) findings.push(found(DB_CONFIG_POLICY, file,
292
+ `${file} storage.file_size_limit must declare a positive byte limit`, { key: 'storage.file_size_limit' }));
293
+ if (config.db?.major_version !== parameters.majorVersion) findings.push(found(DB_CONFIG_POLICY, file,
294
+ `${file} db.major_version must equal the ruleParams.db.majorVersion pin (${parameters.majorVersion})`, { key: 'db.major_version' }));
295
+ return findings;
296
+ }
297
+
298
+ function seedPattern(value) {
299
+ if (typeof value !== 'string' || !value || value.includes('\\') || path.posix.isAbsolute(value) || /^[A-Za-z]:/.test(value)) return null;
300
+ const relative = path.posix.normalize(`supabase/${value}`);
301
+ if (!relative.startsWith('supabase/')) return null;
302
+ const expression = relative.split(/(\*\*|\*|\?)/).map((part) => {
303
+ if (part === '**') return '.*';
304
+ if (part === '*') return '[^/]*';
305
+ if (part === '?') return '[^/]';
306
+ return escapeRegexPart(part);
307
+ }).join('');
308
+ return new RegExp(`^${expression}$`);
309
+ }
310
+
311
+ function seedConfigResult(repoRoot, files, config) {
312
+ const seeds = new Set(files.filter((file) => /^(?:supabase\/(?:seed\.sql|seeds?\/.+\.sql)|\.starcistacks\/[^/]+\/seeds\/.+\.sql)$/.test(file)));
313
+ const seed = config?.db?.seed;
314
+ if (!seed || seed.enabled === false) return { findings: [], seeds };
315
+ const paths = seed.sql_paths ?? ['./seed.sql'];
316
+ const findings = [];
317
+ if (!Array.isArray(paths) || paths.length === 0) return {
318
+ seeds, findings: [found(DB_CONFIG_POLICY, CONFIG_FILE, `${CONFIG_FILE} db.seed.sql_paths must list tracked SQL seed files`, { key: 'db.seed.sql_paths' })],
319
+ };
320
+ for (const value of paths) {
321
+ const pattern = seedPattern(value);
322
+ const matches = pattern ? files.filter((file) => file.endsWith('.sql') && pattern.test(file) && readText(repoRoot, file) !== null) : [];
323
+ if (matches.length === 0) findings.push(found(DB_CONFIG_POLICY, CONFIG_FILE,
324
+ `${CONFIG_FILE} db.seed.sql_paths contains a path with no readable tracked SQL seed file`, { key: 'db.seed.sql_paths' }));
325
+ for (const file of matches) seeds.add(file);
326
+ }
327
+ return { findings, seeds };
328
+ }
329
+
330
+ const EMAIL_LITERAL = /^[A-Z0-9._%+-]+@([A-Z0-9-]+(?:\.[A-Z0-9-]+)+)$/i;
331
+ function piiLiteral(value) {
332
+ if (value.split(/[^\w.%+@-]+/).some((part) => {
333
+ const email = EMAIL_LITERAL.exec(part);
334
+ return email && email[1].toLowerCase() !== 'example.test';
335
+ })) return true;
336
+ if (!/^\+?[\d\s().-]+$/.test(value)) return false;
337
+ const digits = value.replaceAll(/\D/g, '');
338
+ if (digits.length < 9 || digits.length > 15 || /^\d{4}-\d{2}-\d{2}(?:\s|$)/.test(value)) return false;
339
+ return value.startsWith('+') || /[()\s]/.test(value) || /^\d{10,15}$/.test(value);
340
+ }
341
+
342
+ function seedPiiFindings(repoRoot, seeds) {
343
+ const findings = [];
344
+ for (const file of seeds) {
345
+ const text = readText(repoRoot, file);
346
+ if (text === null) continue;
347
+ for (const token of databaseSqlTokens(text)) {
348
+ if (!['string', 'body'].includes(token.t) || !piiLiteral(token.v)) continue;
349
+ findings.push(found(HFS_SEED_PII, file, `${file}:${token.line} contains an email or phone literal; seed data uses synthetic identities and the example.test domain`, { line: token.line }));
350
+ }
351
+ }
352
+ return findings;
353
+ }
354
+
355
+ /** R259 config/content checks, synchronously judged at the app root for both editions. */
356
+ export function databaseConfigFindings({ repoRoot, files = [], all = files, repo = {}, manifest, isRoot, inScope = () => true }) {
357
+ if (isRoot === false || ['be', 'fe'].includes(repo.profile)) return [];
358
+ const parameters = dbParameters(manifest);
359
+ const declaration = readJson(repoRoot, 'hfs.json');
360
+ const edition = repo.edition ?? declaration?.edition ?? 'full';
361
+ const migrations = all.filter((file) => isActiveMigration(file) && file.endsWith('.sql')).sort(byCodeUnit);
362
+ const sources = migrations.map((file) => ({ file, tokens: databaseExecutableTokens(readText(repoRoot, file) ?? '') }));
363
+ const findings = [];
364
+ for (const { file, tokens } of sources) findings.push(...extensionFindings(file, tokens, new Set(parameters.extensions)), ...custodySqlFindings(file, tokens));
365
+ findings.push(...destructiveMigrationFindings({ repoRoot, migrations, files: all }));
366
+ let config = null;
367
+ if (all.includes(CONFIG_FILE)) {
368
+ try { const text = readText(repoRoot, CONFIG_FILE); if (text !== null) config = parseTomlSync(text); } catch { /* L08 owns invalid TOML. */ }
369
+ }
370
+ if (config) findings.push(...schemaConfigFindings(CONFIG_FILE, config, parameters, edition, publicReadSchemas(sources)),
371
+ ...safetyConfigFindings(CONFIG_FILE, config, parameters, declaration?.supabase));
372
+ const seeds = seedConfigResult(repoRoot, all, config);
373
+ findings.push(...seeds.findings, ...seedPiiFindings(repoRoot, seeds.seeds));
374
+ return findings.filter((finding) => inScope(finding.path));
375
+ }
@@ -7,6 +7,15 @@ export const DB_DEFINER_SAFE = 'DB_DEFINER_SAFE';
7
7
  export const DB_STORAGE_POLICY = 'DB_STORAGE_POLICY';
8
8
  export const DB_CONFIG_POLICY = 'DB_CONFIG_POLICY';
9
9
  export const DB_TYPES_DRIFT = 'DB_TYPES_DRIFT';
10
+ export const DB_BASELINE_SHAPE = 'DB_BASELINE_SHAPE';
11
+ export const DB_ARCHIVE_MUTATED = 'DB_ARCHIVE_MUTATED';
12
+ export const DB_PGMQ_DECLARED = 'DB_PGMQ_DECLARED';
13
+ export const DB_CRON_OWNED = 'DB_CRON_OWNED';
14
+ export const DB_EXTENSION_ALLOWED = 'DB_EXTENSION_ALLOWED';
15
+ export const DB_API_EXPOSED = 'DB_API_EXPOSED';
16
+ export const DB_MIGRATION_DESTRUCTIVE = 'DB_MIGRATION_DESTRUCTIVE';
17
+ export const HFS_SEED_PII = 'HFS_SEED_PII';
18
+ export const DB_VAULT_WRITE = 'DB_VAULT_WRITE';
10
19
 
11
20
  export const MIGRATIONS_DIR = 'supabase/migrations';
12
21
  /** The migrations retired at a baseline cutover (slot app.supabase.migrations.archive): never applied, never judged by L02-L08. */
@@ -7,8 +7,16 @@ import { withoutGitLocalEnv } from '../../lib/git.mjs';
7
7
  import { eachInOrder, findInOrder } from '../../lib/in-order.mjs';
8
8
  import { sameText } from '../../lib/same-text.mjs';
9
9
  import { found, readText } from './read.mjs';
10
- import { DB_MIGRATION_SHAPE, isActiveMigration, MIGRATIONS_ARCHIVE_DIR, MIGRATIONS_DIR } from './database-constants.mjs';
11
- const MIGRATION_NAME = /^(\d{14})_([a-z0-9]+(?:-[a-z0-9]+)*)\.sql$/;
10
+ import { DB_MIGRATION_DESTRUCTIVE, DB_MIGRATION_SHAPE, isActiveMigration, MIGRATIONS_ARCHIVE_DIR, MIGRATIONS_DIR } from './database-constants.mjs';
11
+ import path from 'node:path';
12
+ import { fileURLToPath } from 'node:url';
13
+ import { loadTypescript } from '../../lib/package-at.mjs';
14
+
15
+ const HERE = path.dirname(fileURLToPath(import.meta.url));
16
+ const MIGRATION_NAME = /^(\d{14})_([a-z0-9]+(?:-[a-z0-9]+)*|contract_[a-z0-9]+(?:[-_][a-z0-9]+)*|[a-z0-9]+(?:-[a-z0-9]+)*_contract_[a-z0-9]+(?:[-_][a-z0-9]+)*)\.sql$/;
17
+ export const isMigrationName = (name) => MIGRATION_NAME.test(name);
18
+ const RESTAMP_HEADER = /^-- starci:restamp (\d{14}) (\d{14})(?:\r?\n|$)/;
19
+ const withoutRestampHeader = (text) => text?.replace(RESTAMP_HEADER, '') ?? null;
12
20
 
13
21
  /** `<ts14>` -> the UTC instant it names, or null when the digits are no real time (month 13, day 32, ...). */
14
22
  export function migrationStamp(text) {
@@ -84,14 +92,33 @@ function migrationHistory(files, retired, localNames) {
84
92
  return { onHead, latest: stamps.length ? Math.max(...stamps) : null };
85
93
  }
86
94
 
87
- function migrationOrderingFindings(local, history) {
95
+ function migrationOrderingFindings(local, history, restamped = new Set()) {
88
96
  return local.flatMap((entry) => {
89
97
  const stamp = entry.match ? migrationStamp(entry.match[1]) : null;
90
- if (history.onHead.has(entry.name) || stamp === null || history.latest === null || stamp > history.latest) return [];
98
+ if (history.onHead.has(entry.name) || restamped.has(entry.name) || stamp === null || history.latest === null || stamp > history.latest) return [];
91
99
  return [found(DB_MIGRATION_SHAPE, entry.file, `${entry.file} is stamped ${entry.match[1]}, not after every migration already in repository history (latest active ${new Date(history.latest).toISOString()}); a new migration sorts after the committed active chain`, { stamp: entry.match[1], latest: new Date(history.latest).toISOString() })];
92
100
  });
93
101
  }
94
102
 
103
+ /** A restamp changes only the version, and its header proves the explicit old->new map against repository SQL. */
104
+ async function verifiedRestamps(local, files, repoRoot, git, sha) {
105
+ const prefix = `${MIGRATIONS_DIR}/`;
106
+ const active = files.filter(isActiveMigration).map((file) => file.slice(prefix.length));
107
+ const claims = local.map((entry) => ({ ...entry, header: RESTAMP_HEADER.exec(readText(repoRoot, entry.file) ?? '') })).filter((entry) => entry.header);
108
+ const accepted = [];
109
+ await eachInOrder(claims, async (entry) => {
110
+ const [, oldStamp, newStamp] = entry.header;
111
+ if (!entry.match || entry.match[1] !== newStamp || migrationStamp(oldStamp) === null || oldStamp === newStamp) return;
112
+ if (claims.filter((claim) => claim.header[1] === oldStamp).length !== 1) return;
113
+ const names = active.filter((name) => name.startsWith(`${oldStamp}_`) && name.slice(14) === entry.name.slice(14));
114
+ if (names.length !== 1 || local.some((candidate) => candidate.name === names[0])) return;
115
+ const committed = await baseFileText(git, repoRoot, sha, `${prefix}${names[0]}`);
116
+ if (committed === null || !sameText(withoutRestampHeader(readText(repoRoot, entry.file)), withoutRestampHeader(committed))) return;
117
+ accepted.push({ source: names[0], destination: entry.name });
118
+ });
119
+ return accepted;
120
+ }
121
+
95
122
  async function baseMigrationEntryFindings(entry, repoRoot, git, baseSha, onBase) {
96
123
  if (!entry.match) return [];
97
124
  if (onBase.has(entry.name)) {
@@ -124,11 +151,15 @@ export async function migrationShapeFindings({ repoRoot, migrations, archived =
124
151
  });
125
152
  const findings = localMigrationFindings(local);
126
153
  const retired = new Set(archived.map((file) => file.slice(MIGRATIONS_ARCHIVE_DIR.length + 1)));
127
- const history = migrationHistory(await migrationFilesAt(git, repoRoot, 'HEAD'), retired, new Set(local.map((entry) => entry.name)));
128
- findings.push(...migrationOrderingFindings(local, history));
154
+ const headFiles = await migrationFilesAt(git, repoRoot, 'HEAD');
129
155
  const baseSha = await baseShaOf(git, repoRoot, base);
156
+ const baseFiles = baseSha ? await migrationFilesAt(git, repoRoot, baseSha.sha) : [];
157
+ const restamps = await verifiedRestamps(local, baseSha ? baseFiles : headFiles, repoRoot, git, baseSha?.sha ?? 'HEAD');
158
+ for (const restamp of restamps) retired.add(restamp.source);
159
+ const history = migrationHistory(headFiles, retired, new Set(local.map((entry) => entry.name)));
160
+ findings.push(...migrationOrderingFindings(local, history, new Set(restamps.map((entry) => entry.destination))));
130
161
  if (!baseSha) return findings;
131
- const baseNames = (await migrationFilesAt(git, repoRoot, baseSha.sha)).filter(isActiveMigration).map((file) => file.slice(prefix.length));
162
+ const baseNames = baseFiles.filter(isActiveMigration).map((file) => file.slice(prefix.length));
132
163
  const onBase = new Set(baseNames);
133
164
  await eachInOrder(local, async (entry) => {
134
165
  findings.push(...await baseMigrationEntryFindings(entry, repoRoot, git, baseSha, onBase));
@@ -136,3 +167,251 @@ export async function migrationShapeFindings({ repoRoot, migrations, archived =
136
167
  findings.push(...removedMigrationFindings(local, retired, baseNames, baseSha));
137
168
  return findings;
138
169
  }
170
+
171
+ const sqlName = (token) => token && ['word', 'ident'].includes(token.t) ? token.v : null;
172
+ const sqlWord = (token, value) => token?.t === 'word' && token.v === value;
173
+
174
+ function commentEnd(text, start) {
175
+ if (text.startsWith('--', start)) {
176
+ const end = text.indexOf('\n', start + 2);
177
+ return end === -1 ? text.length : end;
178
+ }
179
+ if (!text.startsWith('/*', start)) return null;
180
+ let depth = 1;
181
+ let index = start + 2;
182
+ while (index < text.length && depth > 0) {
183
+ if (text.startsWith('/*', index)) { depth += 1; index += 2; }
184
+ else if (text.startsWith('*/', index)) { depth -= 1; index += 2; }
185
+ else index += 1;
186
+ }
187
+ return index;
188
+ }
189
+
190
+ function quotedToken(text, start, quote, escaped = false) {
191
+ let index = start + 1;
192
+ let value = '';
193
+ while (index < text.length) {
194
+ if (text[index] === quote && text[index + 1] === quote) { value += quote; index += 2; }
195
+ else if (text[index] === quote) return { value, end: index + 1 };
196
+ else if (escaped && text[index] === '\\') { value += text[index + 1] ?? ''; index += 2; }
197
+ else { value += text[index]; index += 1; }
198
+ }
199
+ return { value, end: index };
200
+ }
201
+
202
+ function sqlTokenAt(text, index) {
203
+ const char = text[index];
204
+ if (char === "'" || char === '"') {
205
+ const token = quotedToken(text, index, char);
206
+ return { t: char === '"' ? 'ident' : 'string', v: token.value, end: token.end };
207
+ }
208
+ if (/[eE]/.test(char) && text[index + 1] === "'") {
209
+ const token = quotedToken(text, index + 1, "'", true);
210
+ return { t: 'string', v: token.value, end: token.end };
211
+ }
212
+ const tag = char === '$' ? /^\$(?:[A-Za-z_]\w*)?\$/.exec(text.slice(index))?.[0] : null;
213
+ if (tag) {
214
+ const end = text.indexOf(tag, index + tag.length);
215
+ return { t: 'body', v: text.slice(index + tag.length, end === -1 ? text.length : end), end: end === -1 ? text.length : end + tag.length };
216
+ }
217
+ const word = /^[A-Za-z_][A-Za-z0-9_$]*/.exec(text.slice(index))?.[0];
218
+ if (word) return { t: 'word', v: word.toLowerCase(), end: index + word.length };
219
+ const number = /^\d+(?:\.\d+)?/.exec(text.slice(index))?.[0];
220
+ if (number) return { t: 'number', v: number, end: index + number.length };
221
+ return { t: 'punct', v: char, end: index + 1 };
222
+ }
223
+
224
+ /** Synchronous SQL tokens; literal values stay internal and must never be copied to a finding. */
225
+ export function databaseSqlTokens(text) {
226
+ const tokens = [];
227
+ let index = 0;
228
+ let line = 1;
229
+ while (index < text.length) {
230
+ if (/\s/.test(text[index])) { if (text[index] === '\n') line += 1; index += 1; continue; }
231
+ const end = commentEnd(text, index);
232
+ if (end !== null) { line += text.slice(index, end).split('\n').length - 1; index = end; continue; }
233
+ const token = sqlTokenAt(text, index);
234
+ tokens.push({ t: token.t, v: token.v, line, index });
235
+ line += text.slice(index, token.end).split('\n').length - 1;
236
+ index = token.end;
237
+ }
238
+ return tokens;
239
+ }
240
+
241
+ /** Split executable SQL, retaining function bodies as opaque tokens until their language is known. */
242
+ export function databaseSqlStatements(tokens) {
243
+ const statements = [];
244
+ let current = [];
245
+ for (const token of tokens) {
246
+ if (token.t === 'punct' && token.v === ';') { if (current.length) statements.push(current); current = []; }
247
+ else current.push(token);
248
+ }
249
+ if (current.length) statements.push(current);
250
+ return statements;
251
+ }
252
+
253
+ function executableStatementTokens(statement, depth) {
254
+ const isDo = sqlWord(statement[0], 'do');
255
+ const functionAt = statement.findIndex((token) => sqlWord(token, 'function') || sqlWord(token, 'procedure'));
256
+ const isFunction = sqlWord(statement[0], 'create') && functionAt > 0 && functionAt < 4;
257
+ if (!isDo && !isFunction) return statement;
258
+ const languageAt = statement.findIndex((token) => sqlWord(token, 'language'));
259
+ const language = statement[languageAt + 1]?.v ?? 'sql';
260
+ if (languageAt !== -1 && !['sql', 'plpgsql'].includes(language)) return statement;
261
+ return statement.flatMap((token, index) => {
262
+ const body = token.t === 'body' || (token.t === 'string' && (isDo || sqlWord(statement[index - 1], 'as')));
263
+ if (!body) return [token];
264
+ return databaseExecutableTokens(token.v, depth + 1).map((child) => ({ ...child, line: child.line + token.line - 1 }));
265
+ });
266
+ }
267
+
268
+ /** SQL and PL/pgSQL bodies are scanned; comments, data literals and other function languages are opaque. */
269
+ export function databaseExecutableTokens(text, depth = 0) {
270
+ const tokens = databaseSqlTokens(text);
271
+ if (depth > 8) return tokens;
272
+ return databaseSqlStatements(tokens).flatMap((statement) => [...executableStatementTokens(statement, depth), { t: 'punct', v: ';' }]);
273
+ }
274
+
275
+ function nameAt(tokens, index) {
276
+ const first = sqlName(tokens[index]);
277
+ if (first === null) return null;
278
+ const parts = [first];
279
+ let next = index + 1;
280
+ while (tokens[next]?.v === '.' && sqlName(tokens[next + 1]) !== null) { parts.push(sqlName(tokens[next + 1])); next += 2; }
281
+ return { name: parts.join('.'), leaf: parts.at(-1), next };
282
+ }
283
+
284
+ const skipWords = (tokens, index, words) => {
285
+ let next = index;
286
+ while (words.some((word) => sqlWord(tokens[next], word))) next += 1;
287
+ return next;
288
+ };
289
+
290
+ function dropObjects(tokens, start) {
291
+ const kind = tokens[start + 1]?.v;
292
+ if (!['table', 'role', 'user', 'schema', 'type', 'view', 'materialized', 'sequence', 'function', 'procedure', 'index', 'policy', 'trigger'].includes(kind)) return [];
293
+ let index = skipWords(tokens, start + 2, ['view', 'if', 'exists', 'concurrently']);
294
+ const objects = [];
295
+ while (index < tokens.length) {
296
+ const object = nameAt(tokens, index);
297
+ if (!object) break;
298
+ objects.push({ ...object, kind, line: tokens[start].line });
299
+ if (tokens[object.next]?.v !== ',') break;
300
+ index = object.next + 1;
301
+ }
302
+ return objects;
303
+ }
304
+
305
+ function columnAlterObject(tokens, index, table, line) {
306
+ let next = index + 1;
307
+ if (sqlWord(tokens[next], 'column')) next += 1;
308
+ const column = nameAt(tokens, next);
309
+ if (!column) return null;
310
+ const tail = tokens.slice(column.next);
311
+ const type = sqlWord(tail[0], 'type') || (sqlWord(tail[0], 'set') && sqlWord(tail[1], 'data') && sqlWord(tail[2], 'type'));
312
+ const notNull = sqlWord(tail[0], 'set') && sqlWord(tail[1], 'not') && sqlWord(tail[2], 'null');
313
+ const defaultSet = tokens.some((token, at) => sqlWord(token, 'alter') && sqlName(tokens[at + 1 + Number(sqlWord(tokens[at + 1], 'column'))]) === column.leaf
314
+ && sqlWord(tokens[at + 2 + Number(sqlWord(tokens[at + 1], 'column'))], 'set') && sqlWord(tokens[at + 3 + Number(sqlWord(tokens[at + 1], 'column'))], 'default'));
315
+ if (!type && !(notNull && !defaultSet)) return null;
316
+ return { name: `${table.name}.${column.name}`, leaf: column.leaf, table: table.leaf, kind: 'column', line };
317
+ }
318
+
319
+ function alteredColumn(table, column, line) {
320
+ return { name: `${table.name}.${column.name}`, leaf: column.leaf, table: table.leaf, kind: 'column', line };
321
+ }
322
+
323
+ function tableActionObject(body, index, table, line) {
324
+ if (sqlWord(body[index], 'drop')) {
325
+ const at = skipWords(body, index + 1, ['column', 'constraint', 'if', 'exists']);
326
+ if (sqlWord(body[at], 'default') || sqlWord(body[at], 'not')) return null;
327
+ const column = nameAt(body, at);
328
+ return column ? alteredColumn(table, column, line) : null;
329
+ }
330
+ if (sqlWord(body[index], 'rename')) {
331
+ const column = sqlWord(body[index + 1], 'column') ? nameAt(body, index + 2) : null;
332
+ return column ? alteredColumn(table, column, line) : { ...table, kind: 'table', line };
333
+ }
334
+ return sqlWord(body[index], 'alter') ? columnAlterObject(body, index, table, line) : null;
335
+ }
336
+
337
+ function alterObjects(tokens, start) {
338
+ const table = nameAt(tokens, skipWords(tokens, start + 2, ['if', 'exists', 'only']));
339
+ if (!table) return [];
340
+ const body = tokens.slice(table.next);
341
+ return body.map((token, index) => tableActionObject(body, index, table, tokens[start].line)).filter(Boolean);
342
+ }
343
+
344
+ function truncatedObjects(tokens, start) {
345
+ const objects = [];
346
+ let index = skipWords(tokens, start + 1, ['table', 'only']);
347
+ while (index < tokens.length) {
348
+ const table = nameAt(tokens, index);
349
+ if (!table) break;
350
+ objects.push({ ...table, kind: 'table', line: tokens[start].line });
351
+ if (tokens[table.next]?.v !== ',') break;
352
+ index = skipWords(tokens, table.next + 1, ['only']);
353
+ }
354
+ return objects;
355
+ }
356
+
357
+ function destructiveObjects(statement) {
358
+ const objects = [];
359
+ for (let index = 0; index < statement.length; index += 1) {
360
+ if (sqlWord(statement[index], 'drop')) objects.push(...dropObjects(statement, index));
361
+ else if (sqlWord(statement[index], 'truncate')) objects.push(...truncatedObjects(statement, index));
362
+ else if (sqlWord(statement[index], 'alter') && sqlWord(statement[index + 1], 'table')) objects.push(...alterObjects(statement, index));
363
+ else if (sqlWord(statement[index], 'alter') && ['type', 'schema', 'view', 'sequence', 'function', 'procedure', 'index'].includes(statement[index + 1]?.v)) {
364
+ const object = nameAt(statement, index + 2);
365
+ if (object && statement.slice(object.next).some((token) => sqlWord(token, 'rename'))) objects.push({ ...object, kind: statement[index + 1].v, line: statement[index].line });
366
+ }
367
+ }
368
+ return objects;
369
+ }
370
+
371
+ function sourceNames(ts, text, file) {
372
+ const source = ts.createSourceFile(file, text, ts.ScriptTarget.Latest, true);
373
+ const names = new Set();
374
+ const addSql = (sql) => { for (const token of databaseSqlTokens(sql)) if (sqlName(token) !== null) names.add(token.v); };
375
+ const visit = (node) => {
376
+ if (ts.isStringLiteralLike(node)) addSql(node.text);
377
+ if (ts.isTemplateExpression(node)) { addSql(node.head.text); for (const span of node.templateSpans) addSql(span.literal.text); }
378
+ if (/\.entity\.[cm]?tsx?$/.test(file) && ts.isIdentifier(node)) names.add(node.text);
379
+ ts.forEachChild(node, visit);
380
+ };
381
+ visit(source);
382
+ return names;
383
+ }
384
+
385
+ function destructiveObjectFinding(file, object, contract, references) {
386
+ const reference = references.find(({ names }) => names.has(object.leaf));
387
+ if (contract && !reference) return null;
388
+ const reason = reference && contract ? `${reference.file} still references this object; remove the reference before the contract migration`
389
+ : 'use a *_contract_* migration after the application stops using this object';
390
+ return found(DB_MIGRATION_DESTRUCTIVE, file, `${file}:${object.line} destructively changes ${object.name}; ${reason}`,
391
+ { line: object.line, object: object.name, ...(reference ? { reference: reference.file } : {}) });
392
+ }
393
+
394
+ function destructiveFileFindings(file, text, references) {
395
+ const contract = /_contract_[a-z0-9]+(?:[-_][a-z0-9]+)*\.sql$/.test(file);
396
+ const seen = new Set();
397
+ const objects = databaseSqlStatements(databaseExecutableTokens(text)).flatMap(destructiveObjects);
398
+ return objects.flatMap((object) => {
399
+ const key = `${object.line}:${object.name}`;
400
+ if (seen.has(key)) return [];
401
+ seen.add(key);
402
+ const finding = destructiveObjectFinding(file, object, contract, references);
403
+ return finding ? [finding] : [];
404
+ });
405
+ }
406
+
407
+ /** Destructive changes require a contract migration and no remaining SQL/entity reference in be/src. */
408
+ export function destructiveMigrationFindings({ repoRoot, migrations, files }) {
409
+ const sources = files.filter((file) => /^be\/src\/.+\.(?:sql|entity)\.[cm]?tsx?$/.test(file));
410
+ const ts = sources.length ? loadTypescript(repoRoot, HERE) : null;
411
+ if (sources.length && !ts) return [found(DB_MIGRATION_DESTRUCTIVE, 'hfs.json', 'destructive migration checks require the TypeScript parser to read be/src references')];
412
+ const references = sources.map((file) => ({ file, names: sourceNames(ts, readText(repoRoot, file) ?? '', file) }));
413
+ return migrations.flatMap((file) => {
414
+ const text = readText(repoRoot, file);
415
+ return text === null ? [] : destructiveFileFindings(file, text, references);
416
+ });
417
+ }