@starci/hfs 4.7.32 → 4.7.33

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 (473) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/README.md +13 -6
  3. package/emit/contracts.mjs +4 -3
  4. package/emit/form-bounds-reader.ts.partial +58 -0
  5. package/emit/form-bounds.mjs +155 -0
  6. package/emit/graphql-input-bounds.mjs +97 -0
  7. package/emit/input-bounds.mjs +44 -0
  8. package/emit/operation-schemas.mjs +137 -0
  9. package/emit/operations-worker.mjs +2 -2
  10. package/emit/operations.mjs +52 -16
  11. package/emit/schema-worker.mjs +17 -4
  12. package/emit/type-schema.mjs +17 -5
  13. package/package.json +9 -2
  14. package/release/gate.mjs +18 -23
  15. package/release/index.mjs +1 -1
  16. package/release/push.mjs +4 -5
  17. package/runtime/engine/admission.mjs +331 -0
  18. package/runtime/engine/canonical-json.mjs +14 -0
  19. package/runtime/engine/db/blob.mjs +380 -0
  20. package/runtime/engine/db/event-compact.mjs +73 -0
  21. package/runtime/engine/db/ledger-open.mjs +38 -0
  22. package/runtime/engine/db/ledger-paths.mjs +87 -0
  23. package/runtime/engine/db/ledger.mjs +1068 -0
  24. package/runtime/engine/db/machine-connection.mjs +150 -0
  25. package/runtime/engine/db/machine-open.mjs +60 -0
  26. package/runtime/engine/db/machine-schema.mjs +105 -0
  27. package/runtime/engine/db/machine.mjs +1317 -0
  28. package/runtime/engine/db/provider-reservations.mjs +127 -0
  29. package/runtime/engine/db/provision-authority.mjs +76 -0
  30. package/runtime/engine/db/provision-lock.mjs +29 -0
  31. package/runtime/engine/db/provision-operations.mjs +146 -0
  32. package/runtime/engine/db/ref-value.mjs +67 -0
  33. package/runtime/engine/db/schema/machine.sql +920 -0
  34. package/runtime/engine/db/schema/runtime.sql +1113 -0
  35. package/runtime/engine/refuse.mjs +11 -0
  36. package/runtime/knowledge/hfs/canon-pins.yaml +65 -9
  37. package/runtime/knowledge/hfs/design-truths.yaml +149 -0
  38. package/runtime/knowledge/hfs/fe-stack.yaml +23 -5
  39. package/runtime/knowledge/hfs/rules.yaml +269 -48
  40. package/runtime/knowledge/hfs/slots.yaml +113 -53
  41. package/runtime/knowledge/patterns/be/api.yaml +46 -20
  42. package/runtime/knowledge/patterns/be/event-bus.yaml +19 -12
  43. package/runtime/knowledge/patterns/be/idempotency.yaml +265 -0
  44. package/runtime/knowledge/patterns/be/jobs.yaml +17 -12
  45. package/runtime/knowledge/patterns/be/projections.yaml +2 -2
  46. package/runtime/knowledge/patterns/be/queues.yaml +57 -22
  47. package/runtime/knowledge/patterns/be/reactors.yaml +1 -1
  48. package/runtime/knowledge/patterns/be/realtime.yaml +326 -27
  49. package/runtime/knowledge/patterns/be/saga.yaml +2 -2
  50. package/runtime/knowledge/sonar-gate.yaml +1 -1
  51. package/runtime/modules/kernel/command-policy.yaml +249 -0
  52. package/runtime/modules/kernel/failure-codes.yaml +211 -52
  53. package/runtime/modules/kernel/hook-input.yaml +6 -0
  54. package/runtime/modules/kernel/protected-zone.yaml +81 -0
  55. package/runtime/modules/kernel/removed-vocabulary.yaml +62 -0
  56. package/runtime/modules/kernel/roles.yaml +405 -0
  57. package/runtime/modules/models/registry.yaml +3 -1
  58. package/runtime/modules/models/runtimes.yaml +2 -0
  59. package/runtime/modules/schemas/application-stacks.schema.yaml +10 -2
  60. package/runtime/packages/cli/src/catalog.generated.mjs +46 -59
  61. package/runtime/packages/grammar/scripts/build-stamp.mjs +98 -0
  62. package/runtime/scripts/agent/codex-app-server.mjs +79 -0
  63. package/runtime/scripts/agent/codex-mirror-source.mjs +34 -0
  64. package/runtime/scripts/agent/host-agents.mjs +80 -0
  65. package/runtime/scripts/agent/launch-trust-policy.mjs +53 -0
  66. package/runtime/scripts/agent/trust-launch.mjs +191 -0
  67. package/runtime/scripts/agent/trust.mjs +523 -0
  68. package/runtime/scripts/api/fs/rename-over.mjs +21 -0
  69. package/runtime/scripts/api/fs/temp-path.mjs +8 -0
  70. package/runtime/scripts/api/fs/write-json-file.mjs +10 -0
  71. package/runtime/scripts/api/git/diff-names.mjs +8 -0
  72. package/runtime/scripts/api/git/head-time.mjs +10 -0
  73. package/runtime/scripts/api/git/index-lock.mjs +78 -0
  74. package/runtime/scripts/api/git/is-ancestor.mjs +5 -0
  75. package/runtime/scripts/api/git/log.mjs +5 -0
  76. package/runtime/scripts/api/git/merge-base-query.mjs +5 -0
  77. package/runtime/scripts/api/git/merge-tree.mjs +5 -0
  78. package/runtime/scripts/api/git/rev-list.mjs +5 -0
  79. package/runtime/scripts/api/git/rev-parse.mjs +13 -0
  80. package/runtime/scripts/api/git/status-query.mjs +5 -0
  81. package/runtime/scripts/api/git/worktree-list-query.mjs +5 -0
  82. package/runtime/scripts/api/node/spawn-node.mjs +6 -0
  83. package/runtime/scripts/api/npm/dist-tag.mjs +8 -0
  84. package/runtime/scripts/api/npm/lib.mjs +32 -0
  85. package/runtime/scripts/api/npm/pack-dry-run.mjs +9 -0
  86. package/runtime/scripts/api/npm/run-npm.mjs +6 -0
  87. package/runtime/scripts/api/npm/view.mjs +30 -0
  88. package/runtime/scripts/api/npm/whoami.mjs +8 -0
  89. package/runtime/scripts/api/process/hide-child-windows.mjs +31 -0
  90. package/runtime/scripts/api/process/kill-tree.mjs +22 -0
  91. package/runtime/scripts/api/process/lib.mjs +125 -0
  92. package/runtime/scripts/api/process/probe-guard-command.mjs +74 -0
  93. package/runtime/scripts/api/process/process-list.mjs +28 -0
  94. package/runtime/scripts/api/process/system-tool.mjs +37 -0
  95. package/runtime/scripts/gates/canon-digest.mjs +81 -0
  96. package/runtime/scripts/gates/canon-pins.mjs +16 -0
  97. package/runtime/scripts/gates/commit-proof.mjs +53 -0
  98. package/runtime/scripts/gates/gate-input.mjs +98 -0
  99. package/runtime/scripts/gates/gate.mjs +669 -0
  100. package/runtime/scripts/gates/grammar-dist.mjs +206 -0
  101. package/runtime/scripts/gates/runtime-package-selection.mjs +27 -0
  102. package/runtime/scripts/gates/test-world-run.mjs +254 -0
  103. package/runtime/scripts/gates/type-impact.mjs +277 -0
  104. package/runtime/scripts/gates/type-program.mjs +113 -0
  105. package/runtime/scripts/guards/assigned-command.mjs +12 -0
  106. package/runtime/scripts/guards/call-rights.mjs +47 -0
  107. package/runtime/scripts/guards/command-guard.mjs +468 -0
  108. package/runtime/scripts/guards/command-policy.mjs +342 -0
  109. package/runtime/scripts/guards/critic-reach.mjs +82 -0
  110. package/runtime/scripts/guards/deps-guard.mjs +150 -0
  111. package/runtime/scripts/guards/env-dump-verdict.mjs +89 -0
  112. package/runtime/scripts/guards/git-pathspec.mjs +225 -0
  113. package/runtime/scripts/guards/git-policy.mjs +355 -0
  114. package/runtime/scripts/guards/guards-root.mjs +27 -0
  115. package/runtime/scripts/guards/hook-install.mjs +343 -0
  116. package/runtime/scripts/guards/hook-io.mjs +76 -0
  117. package/runtime/scripts/guards/hook-verdict.mjs +12 -0
  118. package/runtime/scripts/guards/install-verdict.mjs +41 -0
  119. package/runtime/scripts/guards/kernel-seat.mjs +63 -0
  120. package/runtime/scripts/guards/launch-verdict.mjs +74 -0
  121. package/runtime/scripts/guards/op-starci.mjs +16 -0
  122. package/runtime/scripts/guards/op-write-scope.mjs +27 -0
  123. package/runtime/scripts/guards/orca-self-lifecycle.mjs +39 -0
  124. package/runtime/scripts/guards/process-kill-verdict.mjs +106 -0
  125. package/runtime/scripts/guards/protected-zone.mjs +56 -0
  126. package/runtime/scripts/guards/refusals.mjs +27 -0
  127. package/runtime/scripts/guards/release-definition.mjs +185 -0
  128. package/runtime/scripts/guards/release-push-gate.mjs +72 -0
  129. package/runtime/scripts/guards/release-record.mjs +58 -0
  130. package/runtime/scripts/guards/release-suite-mode.mjs +15 -0
  131. package/runtime/scripts/guards/rights.mjs +268 -0
  132. package/runtime/scripts/guards/seat-tools.mjs +47 -0
  133. package/runtime/scripts/guards/shell-commands.mjs +328 -0
  134. package/runtime/scripts/guards/supervisor-seat.mjs +80 -0
  135. package/runtime/scripts/guards/verdict-process.mjs +81 -0
  136. package/runtime/scripts/hfs/architecture/dead-exports.mjs +10 -10
  137. package/runtime/scripts/hfs/architecture/function-spec-providers.mjs +1 -1
  138. package/runtime/scripts/hfs/architecture/operation-contract-roots.mjs +53 -0
  139. package/runtime/scripts/hfs/architecture/reachability.mjs +1 -1
  140. package/runtime/scripts/hfs/architecture/sql-owner.mjs +17 -6
  141. package/runtime/scripts/hfs/architecture/sql-token-forms.mjs +105 -0
  142. package/runtime/scripts/hfs/architecture/sql-tokens.mjs +38 -52
  143. package/runtime/scripts/hfs/architecture/supabase-keys.mjs +261 -0
  144. package/runtime/scripts/hfs/architecture/transport-owner.mjs +49 -3
  145. package/runtime/scripts/hfs/architecture/typescript.mjs +1 -1
  146. package/runtime/scripts/hfs/check.mjs +6 -4
  147. package/runtime/scripts/hfs/coverage-scope.mjs +28 -1
  148. package/runtime/scripts/hfs/db-restamp-verb.mjs +54 -14
  149. package/runtime/scripts/hfs/declaration-shape.mjs +4 -4
  150. package/runtime/scripts/hfs/declaration-slots.mjs +14 -3
  151. package/runtime/scripts/hfs/manifest-shape.mjs +10 -6
  152. package/runtime/scripts/hfs/rule-params-shape.mjs +18 -6
  153. package/runtime/scripts/hfs/rules/a15-findings.mjs +4 -1
  154. package/runtime/scripts/hfs/rules/bypass-markers.mjs +22 -7
  155. package/runtime/scripts/hfs/rules/database-migrations.mjs +69 -14
  156. package/runtime/scripts/hfs/rules/database-queue-sources.mjs +9 -7
  157. package/runtime/scripts/hfs/rules/database-queues.mjs +68 -18
  158. package/runtime/scripts/hfs/rules/edition.mjs +56 -12
  159. package/runtime/scripts/hfs/rules/fe-contract-documents.mjs +1 -10
  160. package/runtime/scripts/hfs/rules/fe-stack.mjs +12 -7
  161. package/runtime/scripts/hfs/rules/lite-proof-lane.mjs +1 -1
  162. package/runtime/scripts/hfs/rules/stacks-lite-backing.mjs +75 -9
  163. package/runtime/scripts/hfs/rules/stacks-runtime.mjs +1 -1
  164. package/runtime/scripts/hfs/rules/stacks-swarm.mjs +15 -4
  165. package/runtime/scripts/hfs/rules/text-integrity.mjs +73 -0
  166. package/runtime/scripts/hfs/runtime-rules/hook-shape.mjs +74 -0
  167. package/runtime/scripts/hfs/runtime-rules/release-notes.mjs +44 -0
  168. package/runtime/scripts/hfs/sealed-members.mjs +3 -3
  169. package/runtime/scripts/hfs/slot-side-problems.mjs +3 -3
  170. package/runtime/scripts/hfs/slots.mjs +2 -1
  171. package/runtime/scripts/hfs/stack-deploy-config.mjs +22 -5
  172. package/runtime/scripts/hfs/stacks-gate.mjs +28 -7
  173. package/runtime/scripts/hfs/view.mjs +2 -1
  174. package/runtime/scripts/lib/app-data.mjs +11 -0
  175. package/runtime/scripts/lib/bypass-markers.mjs +272 -75
  176. package/runtime/scripts/lib/check-format.mjs +7 -0
  177. package/runtime/scripts/lib/cli-arg.mjs +120 -0
  178. package/runtime/scripts/lib/css-rules.mjs +220 -0
  179. package/runtime/scripts/lib/dom-tree.mjs +4 -0
  180. package/runtime/scripts/lib/escape.mjs +16 -0
  181. package/runtime/scripts/lib/event-items.mjs +22 -0
  182. package/runtime/scripts/lib/failure-code-template-errors.mjs +161 -0
  183. package/runtime/scripts/lib/git-dir.mjs +120 -0
  184. package/runtime/scripts/lib/grammar-vendor-parity.mjs +377 -0
  185. package/runtime/scripts/lib/guard-command.mjs +24 -0
  186. package/runtime/scripts/lib/hook-limits.mjs +5 -0
  187. package/runtime/scripts/lib/interrupt-cleanup.mjs +38 -0
  188. package/runtime/scripts/lib/js-lex.mjs +200 -0
  189. package/runtime/scripts/lib/json-formats.mjs +0 -1
  190. package/runtime/scripts/lib/language.mjs +94 -1
  191. package/runtime/scripts/lib/loosening-budgets.mjs +44 -0
  192. package/runtime/scripts/lib/loosening-config.mjs +361 -0
  193. package/runtime/scripts/lib/loosening-state.mjs +223 -0
  194. package/runtime/scripts/lib/menu-parts.mjs +11 -0
  195. package/runtime/scripts/lib/normalize.mjs +25 -0
  196. package/runtime/scripts/lib/op-gate.mjs +15 -0
  197. package/runtime/scripts/lib/redact.mjs +208 -0
  198. package/runtime/scripts/lib/runtime-location.mjs +21 -0
  199. package/runtime/scripts/lib/runtime-package-inventory.mjs +21 -0
  200. package/runtime/scripts/lib/runtime-package-manifests.mjs +84 -0
  201. package/runtime/scripts/lib/runtime-publication-tags.mjs +99 -0
  202. package/runtime/scripts/lib/seat-env.mjs +43 -0
  203. package/runtime/scripts/lib/secret-patterns.mjs +58 -9
  204. package/runtime/scripts/lib/shell-source.mjs +4 -0
  205. package/runtime/scripts/lib/source-phrases.mjs +34 -0
  206. package/runtime/scripts/lib/sqlite.mjs +21 -0
  207. package/runtime/scripts/lib/swarm-stack.mjs +2 -1
  208. package/runtime/scripts/lib/tar-files.mjs +39 -0
  209. package/runtime/scripts/lib/ts-analysis.mjs +10 -0
  210. package/runtime/scripts/lib/worktree-exclude.mjs +35 -0
  211. package/runtime/scripts/lib/worktree-record.mjs +14 -0
  212. package/runtime/scripts/machine/artifact-hold.mjs +52 -0
  213. package/runtime/scripts/machine/ask-receipts.mjs +113 -0
  214. package/runtime/scripts/machine/evidence-store.mjs +223 -0
  215. package/runtime/scripts/machine/home.mjs +216 -0
  216. package/runtime/scripts/machine/host-lock.mjs +184 -0
  217. package/runtime/scripts/machine/lock-recovery.mjs +55 -0
  218. package/runtime/scripts/machine/roles-contract.mjs +90 -0
  219. package/runtime/scripts/machine/runtime-change.mjs +20 -0
  220. package/runtime/scripts/machine/workflow-tree.mjs +122 -0
  221. package/runtime/scripts/reconciler/checklist-items.mjs +7 -0
  222. package/runtime/scripts/reconciler/guard-row.mjs +25 -0
  223. package/runtime/scripts/work/asset-slot.mjs +284 -0
  224. package/runtime/scripts/work/brand/brand-colour.mjs +125 -0
  225. package/runtime/scripts/work/brand/brand-tokens.mjs +167 -0
  226. package/runtime/scripts/work/brand/brand-value-source.mjs +106 -0
  227. package/runtime/scripts/work/brand/brand.mjs +892 -0
  228. package/runtime/scripts/work/draw/draw-dna-anatomy.mjs +60 -0
  229. package/runtime/scripts/work/draw/draw-dna-findings.mjs +256 -0
  230. package/runtime/scripts/work/draw/draw-dna-selector.mjs +38 -0
  231. package/runtime/scripts/work/draw/draw-dna.mjs +483 -0
  232. package/runtime/scripts/work/draw/draw-layer.mjs +305 -0
  233. package/runtime/scripts/work/draw/draw-rationale-page.mjs +237 -0
  234. package/runtime/scripts/work/draw/draw-rationale-ref.mjs +14 -0
  235. package/runtime/scripts/work/draw/draw-rationale.mjs +353 -0
  236. package/runtime/scripts/work/draw/draw-source.mjs +412 -0
  237. package/runtime/scripts/work/draw/draw-taste.mjs +278 -0
  238. package/runtime/scripts/work/draw/draw-typecheck.mjs +72 -0
  239. package/runtime/scripts/work/draw/rationale-findings.mjs +205 -0
  240. package/runtime/scripts/work/draw-grammar.mjs +175 -0
  241. package/runtime/scripts/work/draw-render-page.mjs +188 -0
  242. package/runtime/scripts/work/draw-render.mjs +626 -0
  243. package/runtime/scripts/work/grammar-proposal.mjs +279 -0
  244. package/runtime/scripts/work/png.mjs +237 -0
  245. package/runtime/scripts/work/render-tools.mjs +64 -0
  246. package/runtime/scripts/work/work-io.mjs +119 -0
  247. package/scaffold/add-inbox-lite.mjs +2 -2
  248. package/scaffold/add-plan.mjs +40 -13
  249. package/scaffold/add-queue-lite.mjs +372 -0
  250. package/scaffold/add.mjs +5 -2
  251. package/scaffold/app.mjs +1 -2
  252. package/scaffold/edition-gate.mjs +11 -7
  253. package/scaffold/lite-exports.mjs +10 -0
  254. package/scaffold/pattern-file-slots.mjs +33 -0
  255. package/scaffold/test-world.mjs +1 -1
  256. package/scaffold/yaml-block-edit.mjs +117 -0
  257. package/src/main.mjs +5 -4
  258. package/stack/deploy.mjs +7 -3
  259. package/stack/post-deploy.mjs +48 -0
  260. package/sync/index.mjs +10 -0
  261. package/templates/app/ci-workflows/github/workflows/stack-deploy.yml +1 -1
  262. package/templates/app/ci-workflows-lite/github/workflows/stack-deploy.yml +1 -1
  263. package/templates/app/codegen-graphql.mjs.partial +59 -12
  264. package/templates/app/hooks/husky/pre-commit +2 -2
  265. package/templates/app/hooks/husky/pre-push +10 -3
  266. package/templates/app/package-scripts/redis-dev.json +3 -0
  267. package/templates/app/package-scripts-lite/package.json +1 -0
  268. package/templates/app/skeleton/.starcistacks/application-stacks.yaml +36 -8
  269. package/templates/app/skeleton/.starcistacks/dev/README.md +1 -1
  270. package/templates/app/skeleton/.starcistacks/production/README.md +10 -9
  271. package/templates/app/skeleton/.starcistacks/production/infra/swarm/postgres-init.sh +21 -0
  272. package/templates/app/skeleton/.starcistacks/production/infra/swarm/recovery/restore-proof.md +1 -1
  273. package/templates/app/skeleton/.starcistacks/production/infra/swarm/releases/images.lock.yaml +1 -0
  274. package/templates/app/skeleton/.starcistacks/production/infra/swarm/stack.yaml +47 -5
  275. package/templates/app/skeleton/.starcistacks/production/runtime/env/KEYS.md +5 -3
  276. package/templates/app/skeleton/README.md +2 -0
  277. package/templates/app/skeleton-lite/.starcistacks/application-stacks.yaml +5 -0
  278. package/templates/app/skeleton-lite/.starcistacks/dev/README.md +2 -2
  279. package/templates/app/skeleton-lite/.starcistacks/production/README.md +1 -1
  280. package/templates/app/skeleton-lite/README.md +1 -1
  281. package/templates/be/inbox/platform/client-operation-outcomes.sql.tpl +18 -0
  282. package/templates/be/inbox/platform/event-bus/errors/event-bus.error.ts.tpl +16 -0
  283. package/templates/be/inbox/platform/event-bus/event-bus.contracts.ts.tpl +10 -0
  284. package/templates/be/inbox/platform/event-bus/event-bus.port.ts.tpl +1 -1
  285. package/templates/be/inbox/platform/event-bus/event-bus.service.spec.ts.tpl +15 -3
  286. package/templates/be/inbox/platform/event-bus/event-bus.service.ts.tpl +11 -3
  287. package/templates/be/inbox/platform/event-bus/index.ts.tpl +2 -0
  288. package/templates/be/inbox/platform/event-bus/messages/event-bus.messages.ts.tpl +7 -0
  289. package/templates/be/inbox/platform/event-bus/persistence/event-bus.sql.ts.tpl +2 -2
  290. package/templates/be/inbox/platform/idempotency/errors/idempotency.error.ts.tpl +19 -0
  291. package/templates/be/inbox/platform/idempotency/index.ts.tpl +2 -0
  292. package/templates/be/inbox/platform/idempotency/messages/idempotency.messages.ts.tpl +13 -0
  293. package/templates/be/inbox/platform/inbox/client-operation.policy.spec.ts.tpl +32 -0
  294. package/templates/be/inbox/platform/inbox/client-operation.policy.ts.tpl +17 -0
  295. package/templates/be/inbox/platform/inbox/errors/inbox.error.ts.tpl +22 -0
  296. package/templates/be/inbox/platform/inbox/inbox.port.ts.tpl +21 -0
  297. package/templates/be/inbox/platform/inbox/inbox.service.spec.ts.tpl +334 -11
  298. package/templates/be/inbox/platform/inbox/inbox.service.ts.tpl +55 -4
  299. package/templates/be/inbox/platform/inbox/index.ts.tpl +2 -0
  300. package/templates/be/inbox/platform/inbox/messages/inbox.messages.ts.tpl +15 -0
  301. package/templates/be/inbox/platform/inbox/persistence/inbox.rows.ts.tpl +33 -0
  302. package/templates/be/inbox/platform/inbox/persistence/inbox.sql.ts.tpl +26 -0
  303. package/templates/be/inbox/platform/inbox/request-fingerprint.policy.spec.ts.tpl +76 -0
  304. package/templates/be/inbox/platform/inbox/request-fingerprint.policy.ts.tpl +36 -0
  305. package/templates/be/inbox/platform/migration.sql.tpl +1 -0
  306. package/templates/be/inbox/service.spec.ts.tpl +4 -4
  307. package/templates/be/inbox/service.ts.tpl +3 -1
  308. package/templates/be/inbox/webhook-door.spec.ts.tpl +3 -2
  309. package/templates/be/inbox/webhook-door.ts.tpl +3 -2
  310. package/templates/be/patterns/event-bus/platform/errors/event-bus.error.ts.tpl +3 -0
  311. package/templates/be/patterns/event-bus/platform/event-bus.contracts.ts.tpl +10 -0
  312. package/templates/be/patterns/event-bus/platform/event-bus.port.ts.tpl +2 -2
  313. package/templates/be/patterns/event-bus/platform/event-bus.service.spec.ts.tpl +16 -3
  314. package/templates/be/patterns/event-bus/platform/event-bus.service.ts.tpl +11 -3
  315. package/templates/be/patterns/event-bus/platform/event-envelope.policy.spec.ts.tpl +139 -50
  316. package/templates/be/patterns/event-bus/platform/event-envelope.policy.ts.tpl +25 -2
  317. package/templates/be/patterns/event-bus/platform/event-relay.service.spec.ts.tpl +139 -78
  318. package/templates/be/patterns/event-bus/platform/event-relay.service.ts.tpl +10 -6
  319. package/templates/be/patterns/event-bus/platform/event-runner.service.spec.ts.tpl +262 -159
  320. package/templates/be/patterns/event-bus/platform/event-runner.service.ts.tpl +32 -5
  321. package/templates/be/patterns/event-bus/platform/messages/event-bus.messages.ts.tpl +2 -0
  322. package/templates/be/patterns/event-bus/platform/persistence/entities/event-outbox.entity.ts.tpl +5 -1
  323. package/templates/be/patterns/event-bus/platform/persistence/event-bus.rows.ts.tpl +2 -0
  324. package/templates/be/patterns/event-bus/platform/persistence/event-bus.sql.ts.tpl +4 -4
  325. package/templates/be/patterns/event-bus/platform/persistence/migrations/1789800009000-create-event-outbox.ts.tpl +1 -0
  326. package/templates/be/patterns/idempotency/platform/idempotency/errors/idempotency.error.ts.tpl +19 -0
  327. package/templates/be/patterns/idempotency/platform/idempotency/index.ts.tpl +2 -0
  328. package/templates/be/patterns/idempotency/platform/idempotency/messages/idempotency.messages.ts.tpl +13 -0
  329. package/templates/be/patterns/idempotency/platform/inbox/client-operation.policy.spec.ts.tpl +32 -0
  330. package/templates/be/patterns/idempotency/platform/inbox/client-operation.policy.ts.tpl +17 -0
  331. package/templates/be/patterns/idempotency/platform/inbox/client-outcome-backfill.client.spec.ts.tpl +66 -0
  332. package/templates/be/patterns/idempotency/platform/inbox/client-outcome-backfill.client.ts.tpl +24 -0
  333. package/templates/be/patterns/idempotency/platform/inbox/delivery-claim-backfill.client.spec.ts.tpl +60 -0
  334. package/templates/be/patterns/idempotency/platform/inbox/delivery-claim-backfill.client.ts.tpl +12 -0
  335. package/templates/be/patterns/idempotency/platform/inbox/delivery-claim-release.client.spec.ts.tpl +49 -0
  336. package/templates/be/patterns/idempotency/platform/inbox/delivery-claim-release.client.ts.tpl +12 -0
  337. package/templates/be/patterns/idempotency/platform/inbox/effectful.decorators.ts.tpl +7 -0
  338. package/templates/be/patterns/idempotency/platform/inbox/errors/inbox.error.ts.tpl +22 -0
  339. package/templates/be/patterns/idempotency/platform/inbox/inbox.decorators.ts.tpl +17 -0
  340. package/templates/be/patterns/idempotency/platform/inbox/inbox.module-definition.ts.tpl +7 -0
  341. package/templates/be/patterns/idempotency/platform/inbox/inbox.module.ts.tpl +28 -0
  342. package/templates/be/patterns/idempotency/platform/inbox/inbox.options.ts.tpl +7 -0
  343. package/templates/be/patterns/idempotency/platform/inbox/inbox.port.ts.tpl +44 -0
  344. package/templates/be/patterns/idempotency/platform/inbox/inbox.service.spec.ts.tpl +388 -0
  345. package/templates/be/patterns/idempotency/platform/inbox/inbox.service.ts.tpl +77 -0
  346. package/templates/be/patterns/idempotency/platform/inbox/index.ts.tpl +10 -0
  347. package/templates/be/patterns/idempotency/platform/inbox/messages/inbox.messages.ts.tpl +15 -0
  348. package/templates/be/patterns/idempotency/platform/inbox/persistence/connection.ts.tpl +9 -0
  349. package/templates/be/patterns/idempotency/platform/inbox/persistence/entities/inbox-claim.entity.ts.tpl +25 -0
  350. package/templates/be/patterns/idempotency/platform/inbox/persistence/inbox.rows.ts.tpl +33 -0
  351. package/templates/be/patterns/idempotency/platform/inbox/persistence/inbox.sql.ts.tpl +39 -0
  352. package/templates/be/patterns/idempotency/platform/inbox/persistence/migrations/1789800005000-create-inbox-claims.ts.tpl +21 -0
  353. package/templates/be/patterns/idempotency/platform/inbox/persistence/migrations/1792800000000-client-operation-outcomes.ts.tpl +21 -0
  354. package/templates/be/patterns/idempotency/platform/inbox/request-fingerprint.policy.spec.ts.tpl +76 -0
  355. package/templates/be/patterns/idempotency/platform/inbox/request-fingerprint.policy.ts.tpl +36 -0
  356. package/templates/be/patterns/jobs/platform/fenced.processor.spec.ts.tpl +1 -0
  357. package/templates/be/patterns/jobs/platform/job-claim.service.spec.ts.tpl +4 -3
  358. package/templates/be/patterns/jobs/platform/job-claim.service.ts.tpl +2 -0
  359. package/templates/be/patterns/jobs/platform/job-runner.service.spec.ts.tpl +20 -2
  360. package/templates/be/patterns/jobs/platform/job-runner.service.ts.tpl +1 -0
  361. package/templates/be/patterns/jobs/platform/jobs.contracts.ts.tpl +4 -0
  362. package/templates/be/patterns/jobs/platform-lite/jobs.index.ts.tpl +10 -0
  363. package/templates/be/patterns/jobs/platform-lite/jobs.sql.tpl +44 -0
  364. package/templates/be/patterns/jobs/platform-lite/persistence.jobs.sql.ts.tpl +33 -0
  365. package/templates/be/patterns/queues/platform/bullmq-queue-transport.client.spec.ts.tpl +203 -18
  366. package/templates/be/patterns/queues/platform/bullmq-queue-transport.client.ts.tpl +78 -22
  367. package/templates/be/patterns/queues/platform/errors/queue.error.ts.tpl +19 -0
  368. package/templates/be/patterns/queues/platform/messages/queue.messages.ts.tpl +13 -0
  369. package/templates/be/patterns/queues/platform/persistence.migration.ts.tpl +1 -0
  370. package/templates/be/patterns/queues/platform/persistence.queue-outbox.entity.ts.tpl +4 -0
  371. package/templates/be/patterns/queues/platform/persistence.queue.rows.ts.tpl +2 -0
  372. package/templates/be/patterns/queues/platform/persistence.queue.sql.ts.tpl +4 -4
  373. package/templates/be/patterns/queues/platform/queue-outbox.service.spec.ts.tpl +14 -2
  374. package/templates/be/patterns/queues/platform/queue-outbox.service.ts.tpl +5 -2
  375. package/templates/be/patterns/queues/platform/queue-relay.service.spec.ts.tpl +72 -74
  376. package/templates/be/patterns/queues/platform/queue-relay.service.ts.tpl +1 -1
  377. package/templates/be/patterns/queues/platform/queue-transport.port.ts.tpl +23 -8
  378. package/templates/be/patterns/queues/platform/queue-worker.service.spec.ts.tpl +29 -8
  379. package/templates/be/patterns/queues/platform/queue-worker.service.ts.tpl +7 -3
  380. package/templates/be/patterns/queues/platform/queue.contracts.ts.tpl +5 -0
  381. package/templates/be/patterns/queues/platform/queue.index.ts.tpl +2 -0
  382. package/templates/be/patterns/queues/platform/queue.log-events.ts.tpl +4 -0
  383. package/templates/be/patterns/queues/platform/queue.module.ts.tpl +2 -1
  384. package/templates/be/patterns/queues/platform/queue.port.ts.tpl +1 -1
  385. package/templates/be/patterns/queues/platform-lite/bullmq-queue-transport.client.spec.ts.tpl +417 -0
  386. package/templates/be/patterns/queues/platform-lite/bullmq-queue-transport.client.ts.tpl +172 -0
  387. package/templates/be/patterns/queues/platform-lite/persistence.queue.sql.ts.tpl +12 -0
  388. package/templates/be/patterns/queues/platform-lite/queue-outbox.sql.tpl +32 -0
  389. package/templates/be/patterns/queues/platform-lite/queue-relay.service.spec.ts.tpl +148 -0
  390. package/templates/be/patterns/queues/platform-lite/queue-relay.service.ts.tpl +51 -0
  391. package/templates/be/patterns/queues/platform-lite/queue-transport.port.ts.tpl +163 -0
  392. package/templates/be/patterns/queues/platform-lite/queue.config.ts.tpl +17 -0
  393. package/templates/be/patterns/queues/platform-lite/queue.error.ts.tpl +25 -0
  394. package/templates/be/patterns/queues/platform-lite/queue.index.ts.tpl +8 -0
  395. package/templates/be/patterns/queues/platform-lite/queue.messages.ts.tpl +21 -0
  396. package/templates/be/patterns/queues/platform-lite/queue.module.ts.tpl +74 -0
  397. package/templates/be/patterns/queues/platform-lite/queue.options.ts.tpl +38 -0
  398. package/templates/be/patterns/queues/queue.queue.ts.tpl +2 -2
  399. package/templates/be/patterns/reactors/application.contracts.ts.tpl +2 -0
  400. package/templates/be/patterns/reactors/consumer.ts.tpl +5 -1
  401. package/templates/be/platform/graphql/graphql.contracts.ts +2 -1
  402. package/templates/be/platform/graphql/localize-errors.mapper.spec.ts +1 -1
  403. package/templates/be/skeleton/apps/__app__/src/compose/platform.ts +6 -1
  404. package/templates/be/skeleton/apps/__app__/src/main.ts +7 -4
  405. package/templates/be/skeleton/apps/cli/src/compose/platform.ts +4 -0
  406. package/templates/be/skeleton/apps/cli/src/main.ts +7 -4
  407. package/templates/be/skeleton/src/modules/platform/database/index.ts +1 -1
  408. package/templates/be/skeleton/src/modules/platform/http/errors/http.error.ts +22 -0
  409. package/templates/be/skeleton/src/modules/platform/http/fetch-http-client.client.spec.ts +235 -0
  410. package/templates/be/skeleton/src/modules/platform/http/fetch-http-client.client.ts +85 -0
  411. package/templates/be/skeleton/src/modules/platform/http/graphql-call.mapper.spec.ts +92 -0
  412. package/templates/be/skeleton/src/modules/platform/http/graphql-call.mapper.ts +52 -0
  413. package/templates/be/skeleton/src/modules/platform/http/http.decorators.ts +9 -0
  414. package/templates/be/skeleton/src/modules/platform/http/http.module-definition.ts +7 -0
  415. package/templates/be/skeleton/src/modules/platform/http/http.module.ts +19 -0
  416. package/templates/be/skeleton/src/modules/platform/http/http.options.ts +2 -0
  417. package/templates/be/skeleton/src/modules/platform/http/http.port.ts +40 -0
  418. package/templates/be/skeleton/src/modules/platform/http/index.ts +7 -0
  419. package/templates/be/skeleton/src/modules/platform/http/messages/http.messages.ts +15 -0
  420. package/templates/be/skeleton/src/modules/platform/ids/ids.decorators.ts +9 -0
  421. package/templates/be/skeleton/src/modules/platform/ids/ids.module-definition.ts +7 -0
  422. package/templates/be/skeleton/src/modules/platform/ids/ids.module.ts +19 -0
  423. package/templates/be/skeleton/src/modules/platform/ids/ids.options.ts +2 -0
  424. package/templates/be/skeleton/src/modules/platform/ids/ids.port.ts +5 -0
  425. package/templates/be/skeleton/src/modules/platform/ids/index.ts +4 -0
  426. package/templates/be/skeleton/src/modules/platform/ids/system-ids.service.spec.ts +15 -0
  427. package/templates/be/skeleton/src/modules/platform/ids/system-ids.service.ts +10 -0
  428. package/templates/be/skeleton/src/modules/platform/logging/correlation-request.policy.spec.ts +59 -0
  429. package/templates/be/skeleton/src/modules/platform/logging/correlation-request.policy.ts +21 -0
  430. package/templates/be/skeleton/src/modules/platform/logging/correlation.contracts.ts +16 -0
  431. package/templates/be/skeleton/src/modules/platform/logging/correlation.policy.spec.ts +150 -0
  432. package/templates/be/skeleton/src/modules/platform/logging/correlation.policy.ts +42 -0
  433. package/templates/be/skeleton/src/modules/platform/logging/correlation.port.ts +15 -0
  434. package/templates/be/skeleton/src/modules/platform/logging/correlation.service.spec.ts +23 -0
  435. package/templates/be/skeleton/src/modules/platform/logging/correlation.service.ts +28 -0
  436. package/templates/be/skeleton/src/modules/platform/logging/index.ts +14 -1
  437. package/templates/be/skeleton/src/modules/platform/logging/json-logger.service.spec.ts +39 -6
  438. package/templates/be/skeleton/src/modules/platform/logging/json-logger.service.ts +15 -3
  439. package/templates/be/skeleton/src/modules/platform/logging/logging.decorators.ts +17 -2
  440. package/templates/be/skeleton/src/modules/platform/logging/logging.log-events.ts +13 -1
  441. package/templates/be/skeleton/src/modules/platform/logging/logging.module.ts +13 -3
  442. package/templates/be/skeleton/src/modules/platform/logging/nest-logger.service.spec.ts +69 -0
  443. package/templates/be/skeleton/src/modules/platform/logging/nest-logger.service.ts +48 -0
  444. package/templates/be/skeleton/src/modules/platform/primitives/index.ts +1 -0
  445. package/templates/be/skeleton/src/modules/platform/primitives/record.policy.spec.ts +12 -0
  446. package/templates/be/skeleton/src/modules/platform/primitives/record.policy.ts +3 -0
  447. package/templates/be/skeleton-lite/apps/api/src/compose/platform.ts +6 -1
  448. package/templates/be/skeleton-lite/apps/cli/src/compose/platform.ts +4 -0
  449. package/templates/be/skeleton-lite/src/modules/platform/database/index.ts +1 -0
  450. package/templates/be/skeleton-lite/src/modules/platform/logging/index.ts +14 -1
  451. package/templates/be/skeleton-lite/src/modules/platform/logging/logging.log-events.ts +12 -0
  452. package/templates/be/skeleton-lite/src/modules/platform/primitives/index.ts +2 -0
  453. package/templates/be/test-world/world/test-apps.options.ts.tpl +2 -0
  454. package/templates/be/test-world/world/test-world.config.ts.tpl +1 -0
  455. package/upgrade/check.mjs +12 -135
  456. package/upgrade/file-state.mjs +1 -16
  457. package/upgrade/index.mjs +34 -361
  458. package/runtime/scripts/lib/check-code-loosening.mjs +0 -128
  459. package/runtime/scripts/lib/gate-loosening.mjs +0 -292
  460. package/templates/app/upgrade-full/.starcistacks/application-stacks.yaml +0 -89
  461. package/templates/app/upgrade-full/.starcistacks/dev/README.md +0 -16
  462. package/templates/app/upgrade-full/.starcistacks/dev/infra/compose/api.yaml +0 -13
  463. package/templates/app/upgrade-full/.starcistacks/dev/infra/compose/compose.yaml +0 -3
  464. package/templates/app/upgrade-full/.starcistacks/dev/infra/compose/postgres.yaml +0 -12
  465. package/templates/app/upgrade-full/.starcistacks/dev/infra/metadata.json +0 -7
  466. package/templates/be/skeleton-lite/apps/api/src/main.ts +0 -26
  467. package/templates/be/upgrade-full/apps/cli/src/app.module.ts +0 -12
  468. package/templates/be/upgrade-full/apps/cli/src/cli.env.ts +0 -7
  469. package/templates/be/upgrade-full/apps/cli/src/compose/cli.ts +0 -5
  470. package/templates/be/upgrade-full/apps/cli/src/compose/platform.ts +0 -9
  471. package/templates/be/upgrade-full/apps/cli/src/main.ts +0 -29
  472. package/templates/be/upgrade-full/src/features/cli/cli.module.ts +0 -7
  473. package/templates/be/upgrade-full/src/features/cli/migrate/subs/run.cli.ts +0 -23
@@ -8,6 +8,8 @@ import { stringList, byCodeUnit, nonemptyUniqueList } from '../lib/list.mjs';
8
8
 
9
9
  export const SEMVER = /^(\d+)\.(\d+)\.(\d+)$/;
10
10
  export const NAME = /^[a-z][a-z0-9-]*$/;
11
+ /** An npm package name, scoped or not. */
12
+ export const PACKAGE_NAME = /^(?:@[a-z0-9][a-z0-9._-]*\/)?[a-z0-9][a-z0-9._-]*$/;
11
13
  const ENV_PREFIX =/^[A-Z][A-Z0-9]*(_[A-Z0-9]+)*$/;
12
14
  /** How a bounded context (a connection) is isolated: its own logical database, or its own schema of a shared one. */
13
15
  const CONTEXT_ISOLATIONS = Object.freeze(["database", "schema"]);
@@ -66,14 +68,15 @@ export function unitRolesProblems(be) {
66
68
  return [];
67
69
  }
68
70
 
69
- /** Shape problems of ruleParams.be.logicRoles and thinRoles: each a non-empty list of unique roles of the suffix vocabulary; every unit role is a logic role. */
71
+ /** Shape problems of ruleParams.be.logicRoles, valueRoles and thinRoles: each a non-empty list of unique roles of the suffix vocabulary; every unit role is a logic role. */
70
72
  export function roleListProblems(be) {
71
73
  const bad = [];
72
- for (const key of ['logicRoles', 'thinRoles']) {
74
+ for (const key of ['logicRoles', 'valueRoles', 'thinRoles']) {
73
75
  const list = be[key];
74
76
  if (!strList(list) || !list.length || new Set(list).size !== list.length || !list.every((role) => NAME.test(role))) bad.push(`ruleParams.be.${key} must be a non-empty list of unique kebab-case roles`);
75
77
  else if (Array.isArray(be.suffixes) && list.some((role) => !be.suffixes.includes(role))) bad.push(`ruleParams.be.${key} names a role that is not in ruleParams.be.suffixes`);
76
78
  }
79
+ if (strList(be.valueRoles) && strList(be.logicRoles) && be.valueRoles.some((role) => !be.logicRoles.includes(role))) bad.push('every ruleParams.be.valueRoles role is a logic role (ruleParams.be.logicRoles)');
77
80
  if (strList(be.logicRoles) && Array.isArray(be.unitRoles) && be.unitRoles.some((r) => r?.slot === undefined && !be.logicRoles.includes(r?.role))) bad.push('every ruleParams.be.unitRoles role that is not tied to a slot is a logic role (ruleParams.be.logicRoles)');
78
81
  return bad;
79
82
  }
@@ -359,13 +362,14 @@ function runtimeParamProblems(rp) {
359
362
  const folderKindsValid = list => strList(list) && list.length > 0 && new Set(list).size === list.length && list.every(kind => NAME.test(kind));
360
363
  const kindPairValid = pair => isPlainObject(pair) && Object.keys(pair).sort(byCodeUnit).join() === 'from,to'
361
364
  && ['from', 'to'].every(side => folderKindsValid(pair[side]));
362
- const kindRestrictionValid = kinds => isPlainObject(kinds) && Object.keys(kinds).length > 0
363
- && Object.entries(kinds).every(([target, pair]) => NAME.test(target) && kindPairValid(pair));
365
+ /** A non-empty map whose every key is a name and every value satisfies `valid`. */
366
+ export const namedMapValid = (map, valid) => isPlainObject(map) && Object.keys(map).length > 0
367
+ && Object.entries(map).every(([name, entry]) => NAME.test(name) && valid(entry));
368
+ const kindRestrictionValid = kinds => namedMapValid(kinds, kindPairValid);
364
369
  const slotIdsValid = list => strList(list) && list.length > 0 && new Set(list).size === list.length
365
370
  && list.every(id => SLOT_ID.test(id) || RUNTIME_SLOT_ID.test(id));
366
371
  const slotRestrictionPairValid = pair => isPlainObject(pair) && Object.keys(pair).join() === 'from' && slotIdsValid(pair.from);
367
- const slotRestrictionValid = slots => isPlainObject(slots) && Object.keys(slots).length > 0
368
- && Object.entries(slots).every(([target, pair]) => NAME.test(target) && slotRestrictionPairValid(pair));
372
+ const slotRestrictionValid = slots => namedMapValid(slots, slotRestrictionPairValid);
369
373
 
370
374
  function tierDefinitionProblems(profile, tier, def) {
371
375
  const bad = [];
@@ -4,11 +4,17 @@
4
4
  import { isPlainObject } from '../../engine/plain-object.mjs';
5
5
  import { paramNamesOk } from './param-names.mjs';
6
6
  import { kindParamProblems, scenarioProblem } from './declaration-slots.mjs';
7
- import { NAME, roleListProblems, SCHEMA_AUTHORITIES, unitRolesProblems } from './manifest-shape.mjs';
7
+ import { NAME, namedMapValid, PACKAGE_NAME, roleListProblems, SCHEMA_AUTHORITIES, unitRolesProblems } from './manifest-shape.mjs';
8
8
  import { byCodeUnit, nonemptyUniqueList } from '../lib/list.mjs';
9
+ import { ruleEditionProblems } from './edition-slots.mjs';
9
10
 
10
11
  const PROFILES = ['be', 'fe'];
11
- const BE_OWN = ['infraOwners', 'eventBus', 'specDoubles', 'paramNames', 'suffixes', 'bannedSuffixes', 'contractShape', 'unitRoles', 'logicRoles', 'thinRoles', 'patternScenarios', 'kindPatterns', 'addKinds'];
12
+ const BE_OWN = ['infraOwners', 'eventBus', 'specDoubles', 'paramNames', 'suffixes', 'bannedSuffixes', 'contractShape', 'unitRoles', 'logicRoles', 'valueRoles', 'thinRoles', 'patternScenarios', 'kindPatterns', 'addKinds', 'backing'];
13
+ const backingEntryOk = (entry) => isPlainObject(entry) && Object.keys(entry).every((key) => ['clients', 'images', 'patterns', 'editions'].includes(key))
14
+ && nonemptyUniqueList(entry.clients, (name) => PACKAGE_NAME.test(String(name)))
15
+ && Array.isArray(entry.patterns) && entry.patterns.every((name) => NAME.test(String(name))) && new Set(entry.patterns).size === entry.patterns.length
16
+ && nonemptyUniqueList(entry.images, (name) => PACKAGE_NAME.test(String(name)))
17
+ && ruleEditionProblems(entry, 'backing service').length === 0;
12
18
  const FORM_NAMES = new Set(['call', 'new', 'curried', 'object', 'primitive', 'array']);
13
19
  const blockOk = (v) => isPlainObject(v) && Number.isInteger(v.lines) && v.lines >= 2 && Number.isInteger(v.tokens) && v.tokens >= 1 && Object.keys(v).length === 2;
14
20
  const fileLinesOk = (v) => isPlainObject(v) && Number.isInteger(v.soft) && v.soft >= 1 && typeof v.hardGrowth === 'boolean' && Object.keys(v).length === 2;
@@ -30,7 +36,12 @@ const formsOk = (v) => Array.isArray(v) && v.length > 0 && v.every((x) => FORM_N
30
36
  const doubleOk = (v) => isPlainObject(v) && /^[A-Za-z][A-Za-z0-9]*$/.test(String(v.double)) && formsOk(v.forms);
31
37
  const regexOk = (v) => { try { return typeof v === 'string' && v.length > 0 && Boolean(new RegExp(v)); } catch { return false; } };
32
38
  const dbExtensionsOk = (value) => Array.isArray(value) && value.every((name) => typeof name === 'string' && name.length > 0) && new Set(value).size === value.length;
33
- const dbParamsOk = (db) => db === undefined || (isPlainObject(db) && Object.entries(db).every(([key, value]) => key === 'extensions' && dbExtensionsOk(value)));
39
+ const SQL_NAME = /^[a-z_][a-z0-9_]*$/;
40
+ const queueOutboxOk = (value) => isPlainObject(value) && Object.keys(value).sort(byCodeUnit).join() === 'columns,table'
41
+ && typeof value.table === 'string' && value.table.split('.').length === 2 && value.table.split('.').every((part) => SQL_NAME.test(part))
42
+ && nonemptyUniqueList(value.columns, (column) => SQL_NAME.test(String(column))) && value.columns.includes('queue');
43
+ const DB_PARAMS = { extensions: dbExtensionsOk, queueOutbox: queueOutboxOk };
44
+ const dbParamsOk = (db) => db === undefined || (isPlainObject(db) && Object.entries(db).every(([key, value]) => Object.hasOwn(DB_PARAMS, key) && DB_PARAMS[key](value)));
34
45
 
35
46
  function beSpecificProblems(be, manifest) {
36
47
  const bad = [];
@@ -42,11 +53,12 @@ function beSpecificProblems(be, manifest) {
42
53
  const eventBus = be.eventBus;
43
54
  if (!isPlainObject(eventBus) || Object.keys(eventBus).sort(byCodeUnit).join() !== 'classes,imports' || !memberMap(eventBus.imports, true) || !memberMap(eventBus.classes, false)) bad.push('ruleParams.be.eventBus must be {imports: {<module>: [unique PascalCase members]}, classes: {<module>: [non-empty unique PascalCase classes]}}');
44
55
  if (!paramNamesOk(be.paramNames)) bad.push('ruleParams.be.paramNames must be a non-empty list of unique {type | typeSuffix, names, nameSuffix?} entries');
56
+ if (!namedMapValid(be.backing, backingEntryOk)) bad.push('ruleParams.be.backing must map a backing service name to {clients: [unique package names], patterns: [unique pattern names], images: [unique image repositories], editions?: [unique known editions]}');
45
57
  if (!roleList(be.suffixes)) bad.push('ruleParams.be.suffixes must be a non-empty list of unique kebab-case role suffixes');
46
58
  if (!roleList(be.bannedSuffixes)) bad.push('ruleParams.be.bannedSuffixes must be a non-empty list of unique kebab-case suffixes');
47
59
  else if (roleList(be.suffixes) && be.suffixes.some((x) => be.bannedSuffixes.includes(x))) bad.push('ruleParams.be.suffixes and bannedSuffixes must be disjoint');
48
60
  const sd = be.specDoubles;
49
- if (!isPlainObject(sd) || Object.keys(sd).sort(byCodeUnit).join() !== 'doubles,fallback,kit' || typeof sd.kit !== 'string' || !sd.kit || !Array.isArray(sd.doubles) || !sd.doubles.length || !sd.doubles.every((e) => doubleOk(e) && regexOk(e.token) && Object.keys(e).length === 3) || !doubleOk(sd.fallback) || Object.keys(sd.fallback).length !== 2) bad.push('ruleParams.be.specDoubles must be {kit, doubles: [{token: regex, double, forms}], fallback: {double, forms}} with forms drawn from call, new, curried, object, primitive, array');
61
+ if (!isPlainObject(sd) || Object.keys(sd).sort(byCodeUnit).join() !== 'doubles,fallback,kit' || typeof sd.kit !== 'string' || !sd.kit || !Array.isArray(sd.doubles) || !sd.doubles.length || !sd.doubles.every((e) => doubleOk(e) && regexOk(e.token) && (e.from === undefined || regexOk(e.from)) && Object.keys(e).length === (e.from === undefined ? 3 : 4)) || !doubleOk(sd.fallback) || Object.keys(sd.fallback).length !== 2) bad.push('ruleParams.be.specDoubles must be {kit, doubles: [{token: regex, double, forms}], fallback: {double, forms}} with forms drawn from call, new, curried, object, primitive, array');
50
62
  return bad;
51
63
  }
52
64
 
@@ -59,10 +71,10 @@ export function ruleParamsProblems(m) {
59
71
  // common holds the parameters both sides share; a side may restate one of them (a valid value) as an override, nothing else.
60
72
  // Edition lite: a side's optional `lite` map overrides keys of the same side (never `lite` itself) when the app declares edition lite.
61
73
  if (!sideOk(rp.common, []) || !fileLinesOk(rp.common.fileLines) || !blockOk(rp.common.duplicateBlock)) bad.push('ruleParams.common needs fileLines {soft, hardGrowth} and duplicateBlock {lines >= 2, tokens >= 1}; optional functionLines and featureFiles must be positive integers');
62
- if (!sideOk(rp.be, BE_OWN, ['schemaAuthority', 'lite']) || (rp.be.schemaAuthority !== undefined && !SCHEMA_AUTHORITIES.includes(rp.be.schemaAuthority)) || !liteOk(rp.be.lite, [...BE_OWN, ...SHARED_KEYS, 'schemaAuthority'])) bad.push('ruleParams.be needs infraOwners, eventBus, specDoubles, paramNames, suffixes, bannedSuffixes, contractShape {helper}, unitRoles, logicRoles, thinRoles, patternScenarios, kindPatterns and addKinds (shared parameters live in ruleParams.common, a side may override them); optional: schemaAuthority (' + SCHEMA_AUTHORITIES.join(' | ') + ') and lite (overrides of the same keys)'); else bad.push(...unitRolesProblems(rp.be), ...roleListProblems(rp.be));
74
+ if (!sideOk(rp.be, BE_OWN, ['schemaAuthority', 'lite']) || (rp.be.schemaAuthority !== undefined && !SCHEMA_AUTHORITIES.includes(rp.be.schemaAuthority)) || !liteOk(rp.be.lite, [...BE_OWN, ...SHARED_KEYS, 'schemaAuthority'])) bad.push('ruleParams.be needs infraOwners, eventBus, specDoubles, paramNames, suffixes, bannedSuffixes, contractShape {helper}, unitRoles, logicRoles, thinRoles, patternScenarios, kindPatterns, addKinds and backing (shared parameters live in ruleParams.common, a side may override them); optional: schemaAuthority (' + SCHEMA_AUTHORITIES.join(' | ') + ') and lite (overrides of the same keys)'); else bad.push(...unitRolesProblems(rp.be), ...roleListProblems(rp.be));
63
75
  bad.push(...beSpecificProblems(rp.be, m));
64
76
  if (rp.fe !== undefined && (!sideOk(rp.fe, [], ['lite']) || !liteOk(rp.fe.lite, SHARED_KEYS))) bad.push('ruleParams.fe may only restate the shared parameters of ruleParams.common, and carry a lite override of them');
65
- if (!dbParamsOk(rp.db)) bad.push('ruleParams.db may contain only extensions: a list of unique non-empty extension names');
77
+ if (!dbParamsOk(rp.db)) bad.push('ruleParams.db may contain only extensions (a list of unique non-empty extension names) and queueOutbox ({table: <schema>.<table>, columns: [unique column names including queue]})');
66
78
  }
67
79
  return bad;
68
80
  }
@@ -5,6 +5,7 @@
5
5
  // finders-import-begin
6
6
  import { featureFileFindings } from './feature-files.mjs';
7
7
  import { lexiconFindings } from './lexicon.mjs';
8
+ import { textIntegrityFindings } from './text-integrity.mjs';
8
9
  import { assetsFindings } from './assets.mjs';
9
10
  import { unusedSymbolFindings } from './unused-symbol.mjs';
10
11
  import { databaseBaselineFindings } from './database-baseline.mjs';
@@ -41,11 +42,12 @@ import { feBuildArgsFindings } from './fe-build-args.mjs';
41
42
  /** The failure codes the finders above emit, so `starci app check` ships their labels (check.mjs adds them to CHECK_CODES). Append-only; the coordinator regenerates it at merge time. */
42
43
  export const A15_CODES = [
43
44
  // codes-begin
45
+ 'HFS_TEXT_CORRUPT',
44
46
  'FE_PUBLIC_BUILD_ARG_MISSING',
45
47
  'HFS_WORKFLOW_CONTEXT',
46
48
  'CI_STANDARD',
47
49
  'CI_SECRET_SEALED', 'APP_SECRET_IN_CI', 'CI_DECLARATION_DRIFT',
48
- 'BACKUP_PROOF_MISSING', 'BACKUP_UNDECLARED', 'BE_APP_BOOT_PROOF_MISSING', 'BE_APP_ENV_REQUIRED', 'BE_APP_MODULE_LIST_INLINE', 'BE_LITE_PROOF_LANE_MISSING', 'DB_API_EXPOSED', 'DB_ARCHIVE_MUTATED', 'DB_BASELINE_SHAPE', 'DB_CRON_OWNED', 'DB_EXTENSION_ALLOWED', 'DB_MIGRATION_DESTRUCTIVE', 'DB_PGMQ_DECLARED', 'DB_VAULT_WRITE', 'ENV_DUPLICATE', 'ENV_EXAMPLE_DRIFT', 'ENV_PUBLIC_SEALED', 'ENV_REQUIRED_UNSET', 'ENV_SEALED_UNREAD', 'ENV_VALUE_SHAPE', 'FE_STACK_CORE_MISSING', 'FE_STACK_UNLISTED', 'HFS_AGE_KEY_CUSTODY', 'HFS_APP_LEXICON', 'HFS_APP_WORKFLOW_TRIGGER', 'HFS_ASSET_KIND', 'HFS_ASSET_OVERSIZE', 'HFS_AUDIT_STEP_MISSING', 'HFS_CANON_COPY_DRIFT', 'HFS_CI_TOOL_INSTALL', 'HFS_COVERAGE_BELOW', 'HFS_DB_DEPLOY_UNGUARDED', 'HFS_DEPENDABOT_MISSING', 'HFS_DEPLOY_COMPOSE_ON_VPS', 'HFS_DEPLOY_NO_LOGIC', 'HFS_DEPLOY_SCHEDULER', 'HFS_DEPLOY_WORKFLOW', 'HFS_DOC_COMMAND_UNKNOWN', 'HFS_DOC_ENV_UNKNOWN', 'HFS_DOC_PATH_MISSING', 'HFS_DOC_RULE_UNKNOWN', 'HFS_FEATURE_FILE_BUDGET', 'HFS_HOST_CRON', 'HFS_RELEASE_TAG_MISMATCH', 'HFS_SBOM_MISSING', 'HFS_SCHEDULE_UNDECLARED', 'HFS_SCRIPT_PLAINTEXT_LOAD', 'HFS_SCRIPT_XTRACE', 'HFS_SECRET_OUTSIDE_ENC', 'HFS_SECRET_PLAINTEXT_FILE', 'HFS_SEED_PII', 'HFS_SERVICE_ROLE_SCOPE', 'HFS_UNUSED_SYMBOL', 'HFS_WORKFLOW_PIN', 'STACKS_DEPLOY_CONFIG_INVALID', 'STACKS_DNS_UNDECLARED', 'STACKS_HEALTHCHECK_MISSING', 'STACKS_HOST_UNROUTED', 'STACKS_INGRESS_UNDECLARED', 'STACKS_INGRESS_UNKNOWN', 'STACKS_LITE_SELFHOSTED_DB', 'STACKS_MODEL_UNAVAILABLE', 'STACKS_PLACEMENT_MISSING', 'STACKS_PORT_PUBLISHED', 'STACKS_REALTIME_SCALED', 'STACKS_RELEASE_UNDECLARED', 'STACKS_SECOND_INGRESS', 'STACKS_SECRET_ENV_LITERAL', 'STACKS_SECRET_NOT_EXTERNAL', 'STACKS_SECRET_UNVERSIONED', 'STACKS_SIDECAR_DB_UNSUPPORTED', 'STACKS_SIDECAR_VOLUME_UNDECLARED', 'STACKS_TLS_UNDECLARED', 'STACKS_UPDATE_POLICY_MISSING', 'STACK_TOOLING_REIMPLEMENTED',
50
+ 'BACKUP_PROOF_MISSING', 'BACKUP_UNDECLARED', 'BE_APP_BOOT_PROOF_MISSING', 'BE_APP_ENV_REQUIRED', 'BE_APP_MODULE_LIST_INLINE', 'BE_LITE_PROOF_LANE_MISSING', 'DB_API_EXPOSED', 'DB_ARCHIVE_MUTATED', 'DB_BASELINE_SHAPE', 'DB_CRON_OWNED', 'DB_EXTENSION_ALLOWED', 'DB_MIGRATION_DESTRUCTIVE', 'DB_PGMQ_DECLARED', 'DB_VAULT_WRITE', 'ENV_DUPLICATE', 'ENV_EXAMPLE_DRIFT', 'ENV_PUBLIC_SEALED', 'ENV_REQUIRED_UNSET', 'ENV_SEALED_UNREAD', 'ENV_VALUE_SHAPE', 'FE_STACK_CORE_MISSING', 'FE_STACK_UNLISTED', 'HFS_AGE_KEY_CUSTODY', 'HFS_APP_LEXICON', 'HFS_APP_WORKFLOW_TRIGGER', 'HFS_ASSET_KIND', 'HFS_ASSET_OVERSIZE', 'HFS_AUDIT_STEP_MISSING', 'HFS_CANON_COPY_DRIFT', 'HFS_CI_TOOL_INSTALL', 'HFS_COVERAGE_BELOW', 'HFS_DB_DEPLOY_UNGUARDED', 'HFS_DEPENDABOT_MISSING', 'HFS_DEPLOY_COMPOSE_ON_VPS', 'HFS_DEPLOY_NO_LOGIC', 'HFS_DEPLOY_SCHEDULER', 'HFS_DEPLOY_WORKFLOW', 'HFS_DOC_COMMAND_UNKNOWN', 'HFS_DOC_ENV_UNKNOWN', 'HFS_DOC_PATH_MISSING', 'HFS_DOC_RULE_UNKNOWN', 'HFS_FEATURE_FILE_BUDGET', 'HFS_HOST_CRON', 'HFS_RELEASE_TAG_MISMATCH', 'HFS_SBOM_MISSING', 'HFS_SCHEDULE_UNDECLARED', 'HFS_SCRIPT_PLAINTEXT_LOAD', 'HFS_SCRIPT_XTRACE', 'HFS_SECRET_OUTSIDE_ENC', 'HFS_SECRET_PLAINTEXT_FILE', 'HFS_SEED_PII', 'HFS_SERVICE_ROLE_SCOPE', 'HFS_UNUSED_SYMBOL', 'HFS_WORKFLOW_PIN', 'STACKS_DEPLOY_CONFIG_INVALID', 'STACKS_DNS_UNDECLARED', 'STACKS_HEALTHCHECK_MISSING', 'STACKS_HOST_UNROUTED', 'STACKS_INGRESS_UNDECLARED', 'STACKS_INGRESS_UNKNOWN', 'STACKS_LITE_REDIS_PERSISTENCE', 'STACKS_LITE_SELFHOSTED_DB', 'STACKS_MODEL_UNAVAILABLE', 'STACKS_PLACEMENT_MISSING', 'STACKS_PORT_PUBLISHED', 'STACKS_REALTIME_SCALED', 'STACKS_RELEASE_UNDECLARED', 'STACKS_SECOND_INGRESS', 'STACKS_SECRET_ENV_LITERAL', 'STACKS_SECRET_NOT_EXTERNAL', 'STACKS_SECRET_UNVERSIONED', 'STACKS_SIDECAR_DB_UNSUPPORTED', 'STACKS_SIDECAR_VOLUME_UNDECLARED', 'STACKS_TLS_UNDECLARED', 'STACKS_UPDATE_POLICY_MISSING', 'STACK_TOOLING_REIMPLEMENTED',
49
51
  // codes-end
50
52
  ];
51
53
 
@@ -56,6 +58,7 @@ export const FINDERS = [
56
58
  ciStandardAppFindings,
57
59
  featureFileFindings,
58
60
  lexiconFindings,
61
+ textIntegrityFindings,
59
62
  assetsFindings,
60
63
  unusedSymbolFindings,
61
64
  databaseBaselineFindings,
@@ -4,21 +4,36 @@
4
4
  // ts-nocheck directives), a Sonar exclusion comment, an istanbul / c8 / v8 coverage ignore;
5
5
  // - a skipped, focused or todo test, or a skip option (a front end has no specs, but its source is still judged for the inline disables);
6
6
  // - a failure swallowed in a hook, a script or a workflow: `|| true`, `continue-on-error`, `set +e`, `HUSKY=0`, `--no-verify`,
7
- // `fail_ci_if_error: false`.
8
- // The text is read from the tracked tree here, and from the index by `starci app hygiene` (the pre-commit hook), which uses the same table
9
- // (scripts/lib/bypass-markers.mjs). A managed config edited to loosen is the managed-file drift's (HFS_MANAGED_FILE_DRIFT).
7
+ // `fail_ci_if_error: false`, `--passWithNoTests`;
8
+ // - a gate configuration relaxed: a coverage target or threshold below the canon, a relaxing switch, a disabled ESLint rule, a strictness option
9
+ // turned off, an exception file the design-truths manifest does not list.
10
+ // The tree is read as it stands, by the library of the runtime's own loosening check (scripts/lib/loosening-state.mjs, knowledge/hfs/design-truths.yaml);
11
+ // `starci app hygiene` (the pre-commit hook) reads the staged bypass markers with the same table (scripts/lib/bypass-markers.mjs). The ignore and
12
+ // exclude lists of a managed config are the managed-file drift's (HFS_MANAGED_FILE_DRIFT).
13
+ import fs from 'node:fs';
14
+ import { parseYaml } from '../../../engine/yaml.mjs';
10
15
  import { found, readText } from './read.mjs';
11
16
  import { bypassesIn } from '../../lib/bypass-markers.mjs';
17
+ import { findingLine, stateFindings } from '../../lib/loosening-state.mjs';
12
18
 
13
19
  export const HFS_BYPASS_MARKER = 'HFS_BYPASS_MARKER';
20
+ const TRUTHS_FILE = new URL('../../../knowledge/hfs/design-truths.yaml', import.meta.url);
14
21
 
15
22
  /** The finding message of one marker of `file`. */
16
23
  export const bypassMessage = (file, hit) => `${file}:${hit.line} spells ${hit.label} (${hit.text.slice(0, 80)}); a check is satisfied, never bypassed: fix the cause and delete the marker`;
17
24
 
25
+ const markerFindings = ({ repoRoot, files }) => files.flatMap((file) => {
26
+ const text = readText(repoRoot, file);
27
+ return text === null ? [] : bypassesIn(file, text).map((hit) => found(HFS_BYPASS_MARKER, file, bypassMessage(file, hit), { line: hit.line }));
28
+ });
29
+
30
+ /** The relaxed gate configuration of `files`: the library's findings other than the markers and the lists. */
31
+ const configFindings = ({ repoRoot, files }) => stateFindings({ files, read: (file) => readText(repoRoot, file), truths: parseYaml(fs.readFileSync(TRUTHS_FILE, 'utf8')), lists: false })
32
+ .filter((entry) => entry.kind !== 'bypass' && entry.kind !== 'truths')
33
+ .map((entry) => found(HFS_BYPASS_MARKER, entry.file, `${entry.file}:${entry.line} relaxes a gate (${findingLine(entry)}); a check is satisfied, never relaxed: restore the canon value`, { line: entry.line }));
34
+
18
35
  /** The findings of R303 over the tracked paths `files` of the repository at `repoRoot`. */
19
36
  export function bypassMarkerFindings({ repoRoot, files }) {
20
- return files.flatMap((file) => {
21
- const text = readText(repoRoot, file);
22
- return text === null ? [] : bypassesIn(file, text).map((hit) => found(HFS_BYPASS_MARKER, file, bypassMessage(file, hit), { line: hit.line }));
23
- });
37
+ const markers = markerFindings({ repoRoot, files });
38
+ return markers.concat(configFindings({ repoRoot, files }));
24
39
  }
@@ -11,6 +11,8 @@ import { DB_MIGRATION_DESTRUCTIVE, DB_MIGRATION_SHAPE, isActiveMigration, MIGRAT
11
11
  import path from 'node:path';
12
12
  import { fileURLToPath } from 'node:url';
13
13
  import { loadTypescript } from '../../lib/package-at.mjs';
14
+ import { isConstVariable } from '../../lib/ts-ast.mjs';
15
+ import { lexicalChecker, unwrapExpression } from '../../lib/ts-analysis.mjs';
14
16
 
15
17
  const HERE = path.dirname(fileURLToPath(import.meta.url));
16
18
  const MIGRATION_PLAIN = /[a-z0-9]+(?:-[a-z0-9]+)*/;
@@ -332,10 +334,11 @@ function alteredColumn(table, column, line) {
332
334
 
333
335
  function tableActionObject(body, index, table, line) {
334
336
  if (sqlWord(body[index], 'drop')) {
337
+ const constraint = sqlWord(body[index + 1], 'constraint');
335
338
  const at = skipWords(body, index + 1, ['column', 'constraint', 'if', 'exists']);
336
339
  if (sqlWord(body[at], 'default') || sqlWord(body[at], 'not')) return null;
337
340
  const column = nameAt(body, at);
338
- return column ? alteredColumn(table, column, line) : null;
341
+ return column ? { ...alteredColumn(table, column, line), kind: constraint ? 'constraint' : 'column' } : null;
339
342
  }
340
343
  if (sqlWord(body[index], 'rename')) {
341
344
  const column = sqlWord(body[index + 1], 'column') ? nameAt(body, index + 2) : null;
@@ -378,29 +381,81 @@ function destructiveObjects(statement) {
378
381
  return objects;
379
382
  }
380
383
 
381
- function sourceNames(ts, text, file) {
382
- const source = ts.createSourceFile(file, text, ts.ScriptTarget.Latest, true);
384
+ /** Keep the static parts of one SQL expression together across interpolation and string concatenation. */
385
+ function sourceSqlText(ts, expression, checker, visiting = new Set()) {
386
+ const node = unwrapExpression(ts, expression);
387
+ if (ts.isStringLiteralLike(node)) return node.text;
388
+ if (ts.isTaggedTemplateExpression(node)) return sourceSqlText(ts, node.template, checker, visiting);
389
+ if (ts.isIdentifier(node)) {
390
+ const declaration = checker.getSymbolAtLocation(node)?.valueDeclaration;
391
+ if (!declaration || !ts.isVariableDeclaration(declaration) || !isConstVariable(ts, declaration)
392
+ || !declaration.initializer || visiting.has(declaration)) return null;
393
+ return sourceSqlText(ts, declaration.initializer, checker, new Set([...visiting, declaration]));
394
+ }
395
+ if (ts.isTemplateExpression(node)) return node.head.text + node.templateSpans.map((span) =>
396
+ `${sourceSqlText(ts, span.expression, checker, visiting) ?? ' '}${span.literal.text}`).join('');
397
+ if (ts.isBinaryExpression(node) && node.operatorToken.kind === ts.SyntaxKind.PlusToken) {
398
+ return `${sourceSqlText(ts, node.left, checker, visiting) ?? ' '}${sourceSqlText(ts, node.right, checker, visiting) ?? ' '}`;
399
+ }
400
+ return null;
401
+ }
402
+
403
+ /** SET CONSTRAINTS names have no table qualifier; every matching table's constraint is affected. */
404
+ function globalConstraintNames(statement) {
383
405
  const names = new Set();
384
- const addSql = (sql) => { for (const token of databaseSqlTokens(sql)) if (sqlName(token) !== null) names.add(token.v); };
406
+ for (let index = 0; index < statement.length; index += 1) {
407
+ if (!sqlWord(statement[index], 'set') || !sqlWord(statement[index + 1], 'constraints')) continue;
408
+ let at = index + 2;
409
+ while (at < statement.length && !sqlWord(statement[at], 'all')) {
410
+ const constraint = nameAt(statement, at);
411
+ if (!constraint) break;
412
+ names.add(constraint.leaf);
413
+ if (statement[constraint.next]?.v !== ',') break;
414
+ at = constraint.next + 1;
415
+ }
416
+ }
417
+ return names;
418
+ }
419
+
420
+ function sourceNameScopes(ts, text, file) {
421
+ const source = ts.createSourceFile(file, text, ts.ScriptTarget.Latest, true);
422
+ const checker = lexicalChecker(ts, source);
423
+ const scopes = [];
424
+ const entityNames = /\.entity\.[cm]?tsx?$/.test(file) ? new Set() : null;
425
+ const addSql = (sql) => {
426
+ for (const statement of databaseSqlStatements(databaseSqlTokens(sql))) {
427
+ const names = new Set(statement.filter((token) => sqlName(token) !== null).map((token) => token.v));
428
+ if (entityNames) for (const name of names) entityNames.add(name);
429
+ else {
430
+ const constraints = new Set(statement.flatMap((token, index) =>
431
+ sqlWord(token, 'constraint') && sqlName(statement[index + 1]) !== null ? [statement[index + 1].v] : []));
432
+ scopes.push({ names, constraints, globalConstraints: globalConstraintNames(statement) });
433
+ }
434
+ }
435
+ };
385
436
  const visit = (node) => {
386
- if (ts.isStringLiteralLike(node)) addSql(node.text);
387
- if (ts.isTemplateExpression(node)) { addSql(node.head.text); for (const span of node.templateSpans) addSql(span.literal.text); }
388
- if (/\.entity\.[cm]?tsx?$/.test(file) && ts.isIdentifier(node)) names.add(node.text);
437
+ const sql = sourceSqlText(ts, node, checker);
438
+ if (sql !== null) addSql(sql);
439
+ if (entityNames && ts.isIdentifier(node)) entityNames.add(node.text);
389
440
  ts.forEachChild(node, visit);
390
441
  };
391
442
  visit(source);
392
- return names;
443
+ return entityNames ? [{ names: entityNames, constraints: entityNames, globalConstraints: new Set() }] : scopes;
393
444
  }
394
445
 
395
446
  /**
396
447
  * Whether a be/src source references a destroyed object. A column (`<schema>.<table>.<column>`) is referenced only by a source that
397
- * names both its table and the column, so a same-named column of another table is not a reference; the check stays conservative,
398
- * since any mention of the table beside the column counts. Every other object kind is referenced by its leaf name.
448
+ * names both its table and the column in one SQL statement or entity, so separate queries cannot supply each other's names.
449
+ * SQL constraints require explicit constraint names, while entities retain their table/decorator association.
450
+ * Other object kinds match their leaf name conservatively.
399
451
  */
400
- const referencesObject = (names, object) => names.has(object.leaf) && (object.kind !== 'column' || !object.table || names.has(object.table));
452
+ const referencesObject = (scopes, object) => scopes.some(({ names, constraints, globalConstraints }) =>
453
+ (object.kind === 'constraint' && globalConstraints.has(object.leaf))
454
+ || ((object.kind === 'constraint' ? constraints : names).has(object.leaf)
455
+ && (!['column', 'constraint'].includes(object.kind) || !object.table || names.has(object.table))));
401
456
 
402
457
  function destructiveObjectFinding(file, object, contract, references) {
403
- const reference = references.find(({ names }) => referencesObject(names, object));
458
+ const reference = references.find(({ scopes }) => referencesObject(scopes, object));
404
459
  if (contract && !reference) return null;
405
460
  const reason = reference && contract ? `${reference.file} still references this object; remove the reference before the contract migration`
406
461
  : 'use a *_contract_* migration after the application stops using this object';
@@ -413,7 +468,7 @@ function destructiveFileFindings(file, text, references) {
413
468
  const seen = new Set();
414
469
  const objects = databaseSqlStatements(databaseExecutableTokens(text)).flatMap(destructiveObjects);
415
470
  return objects.flatMap((object) => {
416
- const key = `${object.line}:${object.name}`;
471
+ const key = `${object.line}:${object.kind}:${object.name}`;
417
472
  if (seen.has(key)) return [];
418
473
  seen.add(key);
419
474
  const finding = destructiveObjectFinding(file, object, contract, references);
@@ -426,7 +481,7 @@ export function destructiveMigrationFindings({ repoRoot, migrations, files }) {
426
481
  const sources = files.filter((file) => /^be\/src\/.+\.(?:sql|entity)\.[cm]?tsx?$/.test(file));
427
482
  const ts = sources.length ? loadTypescript(repoRoot, HERE) : null;
428
483
  if (sources.length && !ts) return [found(DB_MIGRATION_DESTRUCTIVE, 'hfs.json', 'destructive migration checks require the TypeScript parser to read be/src references')];
429
- const references = sources.map((file) => ({ file, names: sourceNames(ts, readText(repoRoot, file) ?? '', file) }));
484
+ const references = sources.map((file) => ({ file, scopes: sourceNameScopes(ts, readText(repoRoot, file) ?? '', file) }));
430
485
  return migrations.flatMap((file) => {
431
486
  const text = readText(repoRoot, file);
432
487
  return text === null ? [] : destructiveFileFindings(file, text, references);
@@ -1,4 +1,4 @@
1
- // TypeScript queue constants and scheduler references for migration parity.
1
+ // TypeScript queue constants, event-bus topic queue constants and scheduler references for migration parity.
2
2
  import pg from 'libpg-query';
3
3
  import path from 'node:path';
4
4
  import { fileURLToPath } from 'node:url';
@@ -10,6 +10,7 @@ import { DB_PGMQ_DECLARED } from './database-constants.mjs';
10
10
  await pg.loadModule();
11
11
  const HERE = path.dirname(fileURLToPath(import.meta.url));
12
12
  const QUEUE_FILE = /^(?:be\/)?src\/modules\/queues\/.*\.ts$/;
13
+ const TOPIC_FILE = /^(?:be\/)?src\/modules\/platform\/event-bus\/.*\.ts$/;
13
14
  const SOURCE_FILE = /^(?:be\/)?(?:src|apps)\/.*\.[cm]?[jt]s$/;
14
15
  const TEST_FILE = /(?:\.(?:spec|test)|-spec)\.[cm]?[jt]s$|\/(?:tests|__tests__)\//;
15
16
  const sqlTree = sqlParser(pg);
@@ -64,7 +65,7 @@ export function queueSourceFacts(state, files, inspectSql) {
64
65
  const ts = loadTypescript(state.repoRoot, HERE);
65
66
  const sources = new Map();
66
67
  if (!ts) {
67
- if (files.some((file) => QUEUE_FILE.test(file))) queueProblem(state, DB_PGMQ_DECLARED, 'hfs.json', 'queue declarations require the TypeScript parser');
68
+ if (files.some((file) => QUEUE_FILE.test(file) || TOPIC_FILE.test(file))) queueProblem(state, DB_PGMQ_DECLARED, 'hfs.json', 'queue declarations require the TypeScript parser');
68
69
  return { ts, sources };
69
70
  }
70
71
  for (const file of files.filter((item) => SOURCE_FILE.test(item) && !TEST_FILE.test(item))) {
@@ -72,18 +73,19 @@ export function queueSourceFacts(state, files, inspectSql) {
72
73
  if (text === null) continue;
73
74
  const source = ts.createSourceFile(file, text, ts.ScriptTarget.Latest, true);
74
75
  sources.set(file, sourceBindings(ts, source));
75
- if (QUEUE_FILE.test(file)) addQueueConstants(state, ts, source, file);
76
+ if (QUEUE_FILE.test(file)) addLiteralConstants(state, state.constants, ts, source, file, /(?:^QUEUE$|_QUEUE(?:_NAME)?$|^QUEUE_)/, 'queue names');
77
+ if (TOPIC_FILE.test(file)) addLiteralConstants(state, state.topics, ts, source, file, /_TOPIC_QUEUE$/, 'event-bus topic queue names');
76
78
  addSourceSql(state, ts, source, file, inspectSql);
77
79
  }
78
80
  return { ts, sources };
79
81
  }
80
82
 
81
- function addQueueConstants(state, ts, source, file) {
83
+ function addLiteralConstants(state, into, ts, source, file, name, what) {
82
84
  const visit = (node) => {
83
- if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name) && /(?:^QUEUE$|_QUEUE(?:_NAME)?$|^QUEUE_)/.test(node.name.text)) {
85
+ if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name) && name.test(node.name.text)) {
84
86
  const value = sourceValue(ts, node.initializer);
85
- if (value && ts.isStringLiteralLike(value)) state.constants.set(value.text, file);
86
- else queueProblem(state, DB_PGMQ_DECLARED, file, 'queue names must be literal constants so migration parity can be checked');
87
+ if (value && ts.isStringLiteralLike(value)) into.set(value.text, file);
88
+ else queueProblem(state, DB_PGMQ_DECLARED, file, `${what} must be literal constants so the migrations can be checked against them`);
87
89
  }
88
90
  ts.forEachChild(node, visit);
89
91
  };
@@ -1,4 +1,8 @@
1
- // R258: queue declarations and pg_cron ticks have one migration owner in either edition.
1
+ // R258: the queue is BullMQ on Redis (ruling:R212/case-10), so the active migrations leave no pgmq queue for a queue constant, and
2
+ // every pgmq queue they leave is an event-bus topic queue named by a literal *_TOPIC_QUEUE constant under platform/event-bus. A
3
+ // pg_cron tick has one migration owner and only enqueues: it inserts one row of a queue constant into the queue outbox table of
4
+ // ruleParams.db.queueOutbox. A pgmq.send to a queue that existed when the job was scheduled is the historical form an older
5
+ // migration may still hold; the live job never enqueues that way.
2
6
  // Initialize PostgreSQL's WASM parser at module load; the exported finder itself is synchronous.
3
7
  // SQL and TypeScript ASTs keep comments, quoted identifiers and inert string literals out of the inventory.
4
8
  import pg from 'libpg-query';
@@ -6,6 +10,7 @@ import { sqlStringParts as strings, walkSqlAst as walk, sqlParser, sqlCallArgume
6
10
  import { queueSourceFacts, queueSchedulers, queueProblem as problem } from './database-queue-sources.mjs';
7
11
  import { readJson, readText } from './read.mjs';
8
12
  import { DB_CRON_OWNED, DB_PGMQ_DECLARED, isActiveMigration } from './database-constants.mjs';
13
+ import { loadSlotManifest } from '../slots.mjs';
9
14
 
10
15
  await pg.loadModule();
11
16
 
@@ -192,6 +197,7 @@ function singleCall(tree) {
192
197
  return select.targetList[0].ResTarget?.val?.FuncCall ?? null;
193
198
  }
194
199
 
200
+ /** The historical pgmq.send of an existing queue; only a job's original body may hold it. */
195
201
  function enqueue(state, call) {
196
202
  if (callName(call) !== 'pgmq.send') return null;
197
203
  const queue = sqlLiteral(argument(call, 0, ['queue_name']));
@@ -200,13 +206,52 @@ function enqueue(state, call) {
200
206
  }
201
207
 
202
208
  function pureArguments(call) {
203
- let safe = callsOf(call).every((nested) => PURE_CALLS.has(callName(nested).replace(/^pg_catalog\./, '')));
204
- walk(call.args, (node) => {
209
+ return pureExpressions(call.args);
210
+ }
211
+
212
+ function pureExpressions(nodes) {
213
+ let safe = callsOf(nodes).every((nested) => uuidConstructor(nested) || PURE_CALLS.has(callName(nested).replace(/^pg_catalog\./, '')));
214
+ walk(nodes, (node) => {
205
215
  if (['SubLink', 'SelectStmt', 'InsertStmt', 'UpdateStmt', 'DeleteStmt', 'MergeStmt'].some((key) => node[key])) safe = false;
206
216
  });
207
217
  return safe;
208
218
  }
209
219
 
220
+ /** The queue outbox table and the columns an enqueue may write, from ruleParams.db.queueOutbox; null refuses every outbox enqueue. */
221
+ function queueOutbox(manifest) {
222
+ const declared = manifest?.ruleParams?.db?.queueOutbox;
223
+ return declared ? { table: declared.table, columns: new Set(declared.columns) } : null;
224
+ }
225
+
226
+ /** The queue constant one outbox row enqueues: `insert into <outbox> (queue, ...) values ('<queue constant>', ...)` with pure values. */
227
+ function outboxEnqueue(state, insert) {
228
+ if (!state.outbox || insert.returningClause || insert.returningList || insert.onConflictClause || insert.withClause) return null;
229
+ if (`${insert.relation?.schemaname || 'public'}.${insert.relation?.relname}` !== state.outbox.table) return null;
230
+ const columns = (insert.cols ?? []).map((item) => item.ResTarget?.name);
231
+ if (new Set(columns).size !== columns.length || columns.some((column) => !state.outbox.columns.has(column))) return null;
232
+ const rows = insert.selectStmt?.SelectStmt?.valuesLists ?? [];
233
+ const values = rows.length === 1 ? rows[0].List?.items ?? [] : [];
234
+ const at = columns.indexOf('queue');
235
+ if (at < 0 || values.length !== columns.length) return null;
236
+ const queue = sqlLiteral(values[at]);
237
+ return queue !== null && state.constants.has(queue) && pureExpressions(values) ? queue : null;
238
+ }
239
+
240
+ /** The queue one statement enqueues to (an outbox row of a queue constant; a pgmq.send only when `legacy`), or null. */
241
+ function singleEnqueue(state, tree, legacy) {
242
+ const insert = tree?.stmts?.length === 1 ? tree.stmts[0].stmt?.InsertStmt : undefined;
243
+ if (insert) return outboxEnqueue(state, insert);
244
+ const call = singleCall(tree);
245
+ return legacy && call ? enqueue(state, call) : null;
246
+ }
247
+
248
+ /** PostgreSQL's catalog-owned scalar UUID constructor adds an envelope identity without doing work in the tick. */
249
+ function uuidConstructor(call) {
250
+ return callName(call) === 'pg_catalog.gen_random_uuid' && (call.args?.length ?? 0) === 0
251
+ && (call.agg_order?.length ?? 0) === 0 && !call.agg_filter && !call.over && !call.agg_star
252
+ && !call.agg_distinct && !call.agg_within_group && !call.func_variadic;
253
+ }
254
+
210
255
  function functionBody(fn) {
211
256
  const option = (name) => (fn.options ?? []).find((item) => item.DefElem?.defname === name)?.DefElem?.arg;
212
257
  return { body: strings(option('as')?.List?.items).join(''), language: option('language')?.String?.sval ?? 'sql' };
@@ -239,7 +284,7 @@ function plpgsqlTickTrees(fn, body) {
239
284
  let safe = true;
240
285
  walk(tree, (item) => {
241
286
  for (const key of Object.keys(item)) {
242
- if (key.startsWith('PLpgSQL_stmt_') && !['PLpgSQL_stmt_block', 'PLpgSQL_stmt_perform', 'PLpgSQL_stmt_return'].includes(key)) safe = false;
287
+ if (key.startsWith('PLpgSQL_stmt_') && !['PLpgSQL_stmt_block', 'PLpgSQL_stmt_perform', 'PLpgSQL_stmt_execsql', 'PLpgSQL_stmt_return'].includes(key)) safe = false;
243
288
  if (key === 'PLpgSQL_expr') trees.push(sqlTree(item[key].query));
244
289
  }
245
290
  });
@@ -254,20 +299,22 @@ function tickTrees(fn) {
254
299
  return sqlTree(body)?.stmts?.map((entry) => ({ stmts: [entry] })) ?? null;
255
300
  }
256
301
 
257
- function tickQueues(state, fn) {
302
+ function tickQueues(state, fn, legacy) {
258
303
  const trees = tickTrees(fn.node);
259
304
  if (!trees) return null;
260
- const queues = trees.map((tree) => singleCall(tree)).map((call) => call && enqueue(state, call));
305
+ const queues = trees.map((tree) => singleEnqueue(state, tree, legacy));
261
306
  return queues.length && queues.every(Boolean) ? queues : null;
262
307
  }
263
308
 
264
- function scheduleTarget(state, command) {
265
- const target = singleCall(sqlTree(command));
266
- if (!target) return { tick: null, queues: null };
267
- if (!callName(target).endsWith('_tick')) return { tick: null, queues: enqueue(state, target) };
309
+ /** What a cron command enqueues; `legacy` judges a job's original body, which may still pgmq.send to a queue that existed then. */
310
+ function scheduleTarget(state, command, legacy) {
311
+ const tree = sqlTree(command);
312
+ const target = singleCall(tree);
313
+ if (!target) return { tick: null, queues: singleEnqueue(state, tree, legacy) };
314
+ if (!callName(target).endsWith('_tick')) return { tick: null, queues: legacy ? enqueue(state, target) : null };
268
315
  const tick = qualified(callName(target));
269
316
  const fn = state.functions.get(tick);
270
- const queues = fn && pureArguments(target) && tickQueues(state, fn);
317
+ const queues = fn && pureArguments(target) && tickQueues(state, fn, legacy);
271
318
  return { tick, queues };
272
319
  }
273
320
 
@@ -289,9 +336,9 @@ function scheduleCheck(state, schedule, names, schedulers, ticks) {
289
336
  if (names.has(name)) problem(state, DB_CRON_OWNED, file, 'cron job names must be unique across active migrations');
290
337
  names.add(name);
291
338
  const historical = { ...state, created: schedule.created, functions: schedule.functions };
292
- const { tick, queues } = scheduleTarget(historical, command);
293
- if (!queues) problem(state, DB_CRON_OWNED, file, 'a cron command is one pgmq.send to an existing queue or one existing *_tick function that only enqueues; business DML, dynamic SQL and network calls are refused');
294
- if (state.liveSchedules.get(name) === schedule && !scheduleTarget(state, command).queues) problem(state, DB_CRON_OWNED, file, 'the live cron job targets a missing queue or tick, or its current tick no longer only enqueues');
339
+ const { tick, queues } = scheduleTarget(historical, command, true);
340
+ if (!queues) problem(state, DB_CRON_OWNED, file, 'a cron command inserts one row of a queue constant into the queue outbox, directly or through one existing *_tick function that only does that; business DML, dynamic SQL and network calls are refused');
341
+ if (state.liveSchedules.get(name) === schedule && !scheduleTarget(state, command, false).queues) problem(state, DB_CRON_OWNED, file, 'the live cron job must insert one outbox row of a queue constant (ruling:R212/case-10): it targets a missing queue or tick, still enqueues with pgmq.send, or its current tick no longer only does that');
295
342
  tickOwner(state, tick, file, ticks);
296
343
  const owners = [name, ...(tick ? [tick, tick.split('.').at(-1)] : []), ...[queues ?? []].flat()];
297
344
  if (owners.some((owner) => schedulers.has(owner))) problem(state, DB_CRON_OWNED, file, 'the tick is also declared in an application scheduler list; give it one owner');
@@ -304,20 +351,23 @@ function tickOwner(state, tick, file, ticks) {
304
351
  ticks.add(tick);
305
352
  }
306
353
 
307
- function queueState({ repoRoot, files = [], all = files, repo = {} }) {
354
+ function queueState({ repoRoot, files = [], all = files, repo = {}, manifest }) {
308
355
  const paths = [...new Set(all)];
309
356
  const declaration = readJson(repoRoot, 'hfs.json') ?? repo;
310
357
  const connected = ['be', 'fe'].some((side) => (declaration.sides?.[side]?.connections ?? []).some((item) => item.provider === 'supabase'));
311
358
  if (repo.edition !== 'lite' && declaration.edition !== 'lite' && !declaration.supabase && !connected && !paths.some((file) => file.startsWith('supabase/'))) return null;
312
- const state = { repoRoot, appRole: declaration.supabase?.appRole ?? 'app_be', findings: [], constants: new Map(), created: new Map(), functions: new Map(), schedules: [], activeCron: new Map(), liveSchedules: new Map(), unschedules: [], drops: [], grants: [], queueAcls: new Map() };
359
+ const state = { repoRoot, appRole: declaration.supabase?.appRole ?? 'app_be', findings: [], outbox: queueOutbox(manifest ?? loadSlotManifest()), constants: new Map(), topics: new Map(), created: new Map(), functions: new Map(), schedules: [], activeCron: new Map(), liveSchedules: new Map(), unschedules: [], drops: [], grants: [], queueAcls: new Map() };
313
360
  state.sources = queueSourceFacts(state, paths, inspectCalls);
314
361
  migrationFacts(state, paths);
315
362
  return state;
316
363
  }
317
364
 
318
365
  function validateQueues(state, inScope) {
319
- for (const [queue, file] of state.constants) if (!state.created.has(queue)) problem(state, DB_PGMQ_DECLARED, file, 'a queue constant has no pgmq.create in an active migration');
320
- for (const [queue, file] of state.created) if (!state.constants.has(queue)) problem(state, DB_PGMQ_DECLARED, file, 'pgmq.create has no corresponding queue constant under modules/queues');
366
+ for (const [queue, file] of state.created) {
367
+ if (state.constants.has(queue)) problem(state, DB_PGMQ_DECLARED, file, `pgmq queue ${queue} is the queue constant of ${state.constants.get(queue)}, and the queue is BullMQ on Redis (ruling:R212/case-10): drop it with pgmq.drop_queue in a later migration`);
368
+ else if (!state.topics.has(queue)) problem(state, DB_PGMQ_DECLARED, file, `pgmq queue ${queue} has no event-bus owner: name it in a literal *_TOPIC_QUEUE constant under platform/event-bus, or drop it with pgmq.drop_queue in a later migration`);
369
+ }
370
+ for (const [topic, file] of state.topics) if (!state.created.has(topic)) problem(state, DB_PGMQ_DECLARED, file, `event-bus topic queue ${topic} has no pgmq.create in an active migration`);
321
371
  for (const { node, file, created, executed } of state.grants) grantCheck({ ...state, created: executed ? created : state.created }, node, file);
322
372
  const names = new Set();
323
373
  const ticks = new Set();