@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
@@ -5,7 +5,7 @@ purpose: |-
5
5
  Given a fact another context or a background process must react to, how does it travel? Work inside ONE context stays
6
6
  synchronous: the handler calls that context's domain service in one transaction and no event exists. An event exists only to
7
7
  cross contexts or to trigger asynchronous work (jobs, sagas, reactors, projections). It is a typed event class, published with
8
- `eventBus.publish(event, tx)` by a domain service inside its transaction, and travels one durable path: transactional outbox,
8
+ `eventBus.publish(event, tx, correlationId)` by a domain service inside its transaction, and travels one durable path: transactional outbox,
9
9
  relay, Kafka, consumer with retry and dead letter. `platform/event-bus` is the one owner and the only importer of `kafkajs`.
10
10
  appliesTo:
11
11
  - backend
@@ -15,12 +15,12 @@ family: be
15
15
  slots: [be.transport.message, be.feature.reactors, be.events, be.platform.event-bus]
16
16
  pattern: event-bus
17
17
  provenance:
18
- reviewedAt: 2026-10-02
18
+ reviewedAt: 2026-10-24
19
19
  sources:
20
20
  - knowledge/hfs/README.md
21
21
  - knowledge/hfs/slots.yaml
22
22
  - knowledge/hfs/rules.yaml
23
- - owner decision 2026-10-02 (Kafka only, transactional outbox, typed event classes, publish(event, tx))
23
+ - owner decision 2026-10-02 (Kafka only, transactional outbox, typed event classes)
24
24
  - https://microservices.io/patterns/data/transactional-outbox.html
25
25
  limitations: |-
26
26
  Static checks prove who may publish, with which transaction, and that a consumer is a named door that forwards the event
@@ -85,19 +85,24 @@ rules:
85
85
  kind: mandatory
86
86
  hfsRules: [R152, R90, R87]
87
87
  requirement: |-
88
- `eventBus.publish(event, tx)` is called only in `modules/domain`, with the manager of the transaction that makes the
89
- domain change (`this.entityManager.transaction(async (manager) => { ...; await this.eventBus.publish(event, manager) })`).
88
+ `eventBus.publish(event, tx, correlationId)` is called only in `modules/domain`, with the manager of the transaction that makes the
89
+ domain change (`this.entityManager.transaction(async (manager) => { ...; await this.eventBus.publish(event, manager, request.correlationId) })`).
90
90
  It writes a row in the outbox table of that transaction's connection; the relay hands committed rows to Kafka (topic
91
91
  `events.<service>`, key = aggregate id) and marks them sent. `kafkajs` is imported only by `platform/event-bus`. Nest's
92
92
  `EventBus`, `@EventsHandler`, `@OnEvent` and `EventEmitter2` do not exist, and nothing publishes to Kafka directly.
93
93
  There is no in-process or local transport: unit specs use a recording fake of the `EventBus` port, allowed in `*.spec.ts`.
94
+ The explicit id is validated before SQL and stored in the outbox's `correlation_id` column and envelope
95
+ `metadata.correlationId`, following BE-LOGGING-4. The relay makes the row's `correlation_id` authoritative in outbound metadata.
96
+ A broker envelope whose metadata is genuinely absent may derive its root from a bounded, safe stable event id only
97
+ when its event name and payload have the basic envelope shape. That delivery is promoted to explicit metadata before
98
+ consumer execution, retry or dead-letter publication. Present malformed metadata is rejected.
94
99
  rationale: |-
95
100
  The outbox row commits with the domain change, so a rolled-back change never emits and a committed one is never lost;
96
101
  Kafka then gives ordered, replayable, cross-service delivery.
97
102
  cases:
98
103
  - id: case-1
99
104
  when: A domain service places an order
100
- write: 'await this.entityManager.transaction(async (manager) => { await manager.save(...); await this.eventBus.publish(OrderPlacedEvent.create({ ... }), manager) })'
105
+ write: 'await this.entityManager.transaction(async (manager) => { await manager.save(...); await this.eventBus.publish(OrderPlacedEvent.create({ ... }), manager, request.correlationId) })'
101
106
  verification:
102
107
  automated:
103
108
  - BE_OUTBOX_WRITE_TX
@@ -112,7 +117,7 @@ rules:
112
117
  hfsRules: [R153, R80]
113
118
  requirement: |-
114
119
  A consumer is `transport/message/<event>.consumer.ts` implementing `EventConsumer<E>` with `readonly event = <Event>Event`;
115
- `handle(delivery)` dispatches one command and forwards `delivery.eventId`. Delivery is at least once: the domain service the
120
+ `handle(delivery)` dispatches one command and forwards `delivery.eventId` and `delivery.correlationId`. Delivery is at least once: the domain service the
116
121
  command reaches claims the event id in `platform/inbox` as its first awaited expression, inside its own transaction, so a
117
122
  redelivery is a no-op. A throw schedules a retry with exponential backoff on the retry topic (`events.<service>.retry`);
118
123
  after the event's `attempts` the message goes to the dead-letter topic (`events.<service>.dlq`), which an operator reads
@@ -124,7 +129,7 @@ rules:
124
129
  cases:
125
130
  - id: case-1
126
131
  when: A consumer of order.placed
127
- write: 'order-placed.consumer.ts: handle(delivery) { await this.commandBus.execute(new IssueInvoiceCommand({ request: { eventId: delivery.eventId, ... } })) }'
132
+ write: 'order-placed.consumer.ts: handle(delivery) { await this.commandBus.execute(new IssueInvoiceCommand({ request: { eventId: delivery.eventId, correlationId: delivery.correlationId, ... } })) }'
128
133
  verification:
129
134
  automated:
130
135
  - BE_EVENT_CONSUMER_SHAPE
@@ -283,12 +288,15 @@ files:
283
288
  role: "The consumed event's e2e flow; titles event-bus/<scenario>: ... prove the pattern."
284
289
  spec: event-bus/atomic-with-transaction, event-bus/relay-only-after-commit, event-bus/redelivery-then-dead-letter, event-bus/duplicate-delivery-is-a-noop
285
290
  rules: [BE_PATTERN_SPEC_MISSING]
286
- # Edition lite (R212, owner ruling 2026-10-14): the same ports and rules, a Supabase adapter behind them.
291
+ # Edition lite (R212): the same ports and rules, a Supabase adapter behind them. ruling:R212/case-10 moved the lite queue to Redis
292
+ # because full's queue is Redis; full's event bus is Kafka, so the lite event-bus adapter below is unchanged by that ruling.
287
293
  liteAdapter:
288
294
  summary: >-
289
- Under lite the event bus keeps typed events, the vendored contract, publish(event, tx) in the domain transaction, the outbox,
295
+ Under lite the event bus keeps typed events, the vendored contract, publish(event, tx, correlationId) in the domain transaction, the outbox,
290
296
  the inbox and thin consumers; Kafka is replaced by pgmq-backed topics, or by in-process delivery after commit for a back end
291
- with a single api app.
297
+ with a single api app. Each consumer group of a topic has its own pgmq queue `<service>__<event>__<group>`, created by a
298
+ supabase migration, named by a literal `<GROUP>_TOPIC_QUEUE` constant under platform/event-bus (its owner, R258) and
299
+ granted only to the application role, so each group keeps its own cursor and dead letter.
292
300
  ports:
293
301
  - {port: EventPublisher (platform/event-bus), adapter: the same outbox row in the domain transaction}
294
302
  - {port: relay, adapter: "fans each committed event out with pgmq.send to one queue per consumer group of its topic, marking the row in the same transaction"}
@@ -297,4 +305,3 @@ liteAdapter:
297
305
  proof: >-
298
306
  Unit specs as full; declaring event-bus requires integration specs titled `event-bus/<scenario>: ...` for every declared
299
307
  scenario on `starci supabase start` (R284). Lite has no e2e world.
300
- see: [knowledge/patterns/be/supabase-queues.yaml]
@@ -0,0 +1,265 @@
1
+ schema: starci/knowledge-source@1
2
+ id: be.idempotency
3
+ title: Inbox dedupe and client operation receipts
4
+ purpose: |-
5
+ Given a door a delivery can repeat through - a signed webhook, a queue message, a relayed outbox row - how does the receiver
6
+ make a repeat a no-op instead of a second charge, a second email or a second state transition? One inbox table keyed by
7
+ (source, event id) sits in front of every such door, reached through `InjectInbox()`. Effectful client commands use the
8
+ same owner to bind a key to the request fingerprint and replay the committed outcome.
9
+ appliesTo:
10
+ - backend
11
+ family: be
12
+ # The slot ids this topic teaches (knowledge/hfs/slots.yaml); scripts/gates/read-digest.mjs inverts these
13
+ # into the READ-step slot -> pattern-topic map (knowledge/op-gate.yaml kinds), each slot's topics in index.yaml order.
14
+ slots: [be.transport.message, be.feature.reactors, be.integrations, be.feature.application, be.transport.graphql, be.transport.http, be.platform]
15
+ pattern: idempotency
16
+ provenance:
17
+ reviewedAt: 2026-10-24
18
+ sources:
19
+ - knowledge/hfs/README.md
20
+ - knowledge/hfs/slots.yaml
21
+ - knowledge/hfs/rules.yaml
22
+ - owner-locked BE convention 2026-09-30
23
+ - knowledge/patterns/be/persistence.yaml
24
+ - owner decision 2026-10-24 (effectful client commands require marker and lint)
25
+ - https://www.postgresql.org/docs/16/sql-insert.html (read 2026-10-24)
26
+ - https://www.postgresql.org/docs/16/transaction-iso.html (read 2026-10-24)
27
+ - https://raw.githubusercontent.com/typeorm/typeorm/0.3.31/src/driver/postgres/PostgresQueryRunner.ts (read 2026-10-24)
28
+ limitations: |-
29
+ Static checks judge the delivery entry of the service a thin door calls: the genuine Inbox receiver, the first await,
30
+ and the return on false. For a sole returned or awaited EntityManager transaction, they also inspect its actual inline
31
+ callback, require the claim in its first statement, and bind the claim's manager to that callback's parameter.
32
+ They do not prove getter effects, called helpers, database rollback or broker delivery; service specs and the real test world
33
+ prove failure and redelivery behaviour. Non-inline callbacks and dynamic isolation expressions are not static proof here.
34
+ The client rule checks the marked command contract, transport key validation and the handler's resolved domain delegation
35
+ into a transaction-owned runClient call. Whether an operation has effects, its fingerprint covers all request meaning,
36
+ and its remote effects are separately protected remain design review and behavioral proof obligations.
37
+ rules:
38
+ - id: BE-IDEMPOTENCY-1
39
+ title: The delivery service claims the event before it acts
40
+ kind: mandatory
41
+ hfsRules: [R80, R85]
42
+ requirement: |-
43
+ A consumer (`transport/message/<event>.consumer.ts`) is a thin door that forwards the delivery's `eventId` through one
44
+ command to the domain service. A signed webhook verifies first and calls one domain method that durably records the
45
+ inbox receipt and outbox event; only a reactor or job dispatches its processing command after commit (webhooks.yaml).
46
+ A public delivery method
47
+ of that service (its first parameter carries `eventId`) takes `@InjectInbox() private readonly inbox: Inbox`, claims
48
+ `(source, eventId)` first and returns when the claim answers `false`. When the service owns an EntityManager transaction,
49
+ the first awaited work in the transaction callback is `this.inbox.claim(source, eventId, manager)`, with that callback's
50
+ own manager. The claim, domain writes and outbox rows commit or roll back together; never move the claim to the shared
51
+ manager outside the transaction to satisfy a static check. A duplicate return ends the callback and no outer delivery
52
+ work follows it. `source` names the provider or publisher; `eventId` comes from the delivery and is never generated by
53
+ the receiver. The rule identifies the Inbox and EntityManager by their declared type origins, not variable names.
54
+ An outbox relay that only produces is not a delivery service and is not asked for a claim.
55
+ rationale: |-
56
+ A sender that gets no timely reply resends: a payment provider retries a webhook, a queue redelivers a message it never
57
+ saw acknowledged. Identifying the receiver by type removes the keyword match a renamed variable could satisfy.
58
+ cases:
59
+ - id: case-1
60
+ when: A signed webhook controller
61
+ write: 'the webhook door (webhooks.yaml) verifies the signature and calls one domain recording intake; that method claims (PAYOS_SOURCE, eventId) first in its transaction, returns when the claim answers false, and saves the durable receipt with its outbox event. A reactor or job consumes the committed receipt and dispatches the processing command; the webhook door, HTTP module and called intake never dispatch on a command bus.'
62
+ - id: case-2
63
+ when: A consumer
64
+ write: 'be/src/features/api/billing/transport/message/payment-captured.consumer.ts forwards delivery.eventId in one command; its domain service claims first inside its own transaction with the callback manager, then writes and publishes through that manager.'
65
+ verification:
66
+ automated:
67
+ - BE_INBOX_DEDUPE_MISSING
68
+ - BE_RAW_INJECT
69
+ manual:
70
+ - Trace that the claim executes on every path before the side effect, including an early return added later.
71
+ relatedExamples: []
72
+ - id: BE-IDEMPOTENCY-2
73
+ title: The inbox is one table, owned by platform, shared by every door
74
+ kind: mandatory
75
+ hfsRules: [R80, R35, R34]
76
+ requirement: |-
77
+ `platform/inbox` owns one table, `inbox_claims(source varchar(200), event_id varchar(200),
78
+ claimed_at timestamptz not null, request_fingerprint varchar(64) null, outcome jsonb null)`, with a composite primary key on `(source, event_id)`, created by a migration under
79
+ `platform/inbox/persistence/migrations/`. `Inbox.claim(source, eventId, tx?)` inserts atomically on the supplied
80
+ transaction manager, or on the configured manager when no caller transaction is supplied. A conflict-safe insert
81
+ (`ON CONFLICT (source, event_id) DO NOTHING RETURNING event_id`) returns `true` for a new row and `false` for a
82
+ duplicate without aborting the caller's transaction. It never reads first and writes after, which races two concurrent
83
+ deliveries of the same event. Lite uses its existing `public.inbox` table with the same client receipt columns,
84
+ owned by a forward-only Supabase migration. A client receipt extends this table; no capability creates another
85
+ dedupe table or unique request-key constraint to compete with it.
86
+ rationale: |-
87
+ One shared table with one owner keeps the guarantee "this (source, event id) claims at most once" in one place, and
88
+ the atomic conflict-safe insert makes the claim safe under concurrent delivery and keeps a duplicate transaction usable.
89
+ cases:
90
+ - id: case-1
91
+ when: The migration
92
+ write: 'The platform inbox migration creates inbox_claims with PRIMARY KEY (source, event_id); its client-operation migration adds request_fingerprint and outcome to that same table.'
93
+ - id: case-2
94
+ when: The adapter
95
+ write: 'async claim(source, eventId, tx?) { const manager = tx ?? this.managers[0]; const rows = await manager.query(CLAIM_EVENT, [source, eventId, this.clock.now()]); return rows.length > 0 } // CLAIM_EVENT is the conflict-safe insert in the inbox owner persistence file.'
96
+ verification:
97
+ automated:
98
+ - BE_INBOX_DEDUPE_MISSING
99
+ - BE_SCHEMA_OWNER
100
+ - BE_SCHEMA_AUTHORITY
101
+ manual:
102
+ - Confirm no capability declares a second inbox-shaped table instead of using this one.
103
+ relatedExamples: [idempotency]
104
+ - id: BE-IDEMPOTENCY-3
105
+ title: Effectful client commands require a key and replay their committed outcome
106
+ kind: mandatory
107
+ hfsRules: [R310, R155, R83, R87]
108
+ requirement: |-
109
+ Mark every command that initiates an effectful client operation with the `Effectful()` decorator exported by
110
+ `platform/inbox` (deciding that a command is effectful is a design-review judgment: R310 judges the commands that carry
111
+ the marker, it cannot find one that lacks it, and an app that never marks a command declares no idempotency
112
+ pattern). Its request carries a required string `idempotencyKey`. GraphQL accepts a required input field;
113
+ REST maps `Idempotency-Key` through a validated input. Validate the key with `IsUUID()` or with `IsString()`,
114
+ `MinLength(1)` and `MaxLength(64)`. The client retains one key across retries of one intended operation and uses a new
115
+ key for a new intent. Internal delivery commands retain the delivery identity of BE-IDEMPOTENCY-1.
116
+ The thin handler forwards the key to one domain service. That service returns its EntityManager transaction,
117
+ whose callback returns `inbox.runClient({ principalId, operation, idempotencyKey, request }, manager, effect)`.
118
+ The principal comes from verified authentication; the operation is a stable operation name. `request` contains every
119
+ business input that changes the meaning of the operation, excluding the key. Inbox canonicalizes the JSON object
120
+ keys and hashes the request, then atomically claims `(source = client:<principalId>:<operation>, event_id = key)`
121
+ through that callback manager. A matching repeat returns the stored complete outcome, including a domain refusal,
122
+ without invoking `effect`; a different fingerprint throws `IDEMPOTENCY_KEY_REUSED`, mapped to a conflict.
123
+ The claim, effect's local writes and outbox rows, and JSON outcome commit together. An exception rolls them all back.
124
+ The outcome is JSON data; undefined, non-finite numbers and non-JSON objects are refused. A concurrent claimant waits
125
+ for the first transaction and replays its committed outcome or claims after its rollback. External calls use an
126
+ outbox or their provider's supported idempotency contract; the local receipt does not prove remote deduplication.
127
+ The domain owns the transaction (BE-PERSISTENCE-10); no interceptor owns a claim or transaction.
128
+ A scheduled fenced job sweeps completed client receipts in bounded batches after the configured retention window
129
+ measured from their first claim.
130
+ Delivery claims are preserved. A key may identify a new intent after its receipt is swept, so clients must keep
131
+ retries within the advertised retention window. Declare `idempotency` in `hfs.json` `sides.be.patterns` and prove
132
+ `idempotency/repeat-returns-same-outcome` and `idempotency/reused-key-other-body-is-refused` on real Postgres through
133
+ the full test world or lite's local integration lane.
134
+ rationale: |-
135
+ A timeout leaves a client unsure whether a mutation committed. The receipt binds the caller's intent to its durable
136
+ result while a request fingerprint prevents the same key from silently authorizing different work.
137
+ cases:
138
+ - id: case-1
139
+ when: Checkout adds a cart item
140
+ write: 'Mark AddCartItemCommand with Effectful(); forward its validated key and fingerprint { productId, quantity } to runClient inside the checkout transaction, and return the stored Outcome on retry.'
141
+ - id: case-2
142
+ when: A transaction fails before commit
143
+ write: 'Let the exception escape the transaction; the receipt and mutation roll back together so the same key can retry.'
144
+ - id: case-3
145
+ when: Retention expires
146
+ write: 'A FencedProcessor delegates to the inbox retention domain service; the inbox owner deletes only completed client receipts older than the configured window in a bounded batch.'
147
+ verification:
148
+ automated:
149
+ - BE_MUTATION_IDEMPOTENT
150
+ - BE_PATTERN_SPEC_MISSING
151
+ - BE_UNNAMED_DATA_ACCESS
152
+ - BE_CQRS_SHAPE
153
+ - BE_READONLY_BOUNDARY
154
+ manual:
155
+ - Review the effectful designation, authenticated principal, operation identity, complete request meaning and JSON outcome.
156
+ - Exercise simultaneous identical requests, rollback and a changed body under the same key on the real database.
157
+ - Confirm retention and provider recovery behavior match the advertised client contract.
158
+ relatedExamples: [idempotency]
159
+ files:
160
+ - path: src/modules/platform/idempotency/index.ts
161
+ slot: be.platform
162
+ template: idempotency/platform/idempotency/index.ts.tpl
163
+ - path: src/modules/platform/idempotency/errors/idempotency.error.ts
164
+ slot: be.platform
165
+ template: idempotency/platform/idempotency/errors/idempotency.error.ts.tpl
166
+ - path: src/modules/platform/idempotency/messages/idempotency.messages.ts
167
+ slot: be.domain.messages
168
+ template: idempotency/platform/idempotency/messages/idempotency.messages.ts.tpl
169
+ - path: src/modules/platform/inbox/index.ts
170
+ slot: be.platform
171
+ template: idempotency/platform/inbox/index.ts.tpl
172
+ - path: src/modules/platform/inbox/inbox.port.ts
173
+ slot: be.platform
174
+ template: idempotency/platform/inbox/inbox.port.ts.tpl
175
+ - path: src/modules/platform/inbox/inbox.decorators.ts
176
+ slot: be.platform
177
+ template: idempotency/platform/inbox/inbox.decorators.ts.tpl
178
+ - path: src/modules/platform/inbox/effectful.decorators.ts
179
+ slot: be.platform
180
+ template: idempotency/platform/inbox/effectful.decorators.ts.tpl
181
+ - path: src/modules/platform/inbox/inbox.options.ts
182
+ slot: be.platform
183
+ template: idempotency/platform/inbox/inbox.options.ts.tpl
184
+ - path: src/modules/platform/inbox/inbox.module-definition.ts
185
+ slot: be.platform
186
+ template: idempotency/platform/inbox/inbox.module-definition.ts.tpl
187
+ - path: src/modules/platform/inbox/inbox.module.ts
188
+ slot: be.platform
189
+ template: idempotency/platform/inbox/inbox.module.ts.tpl
190
+ - path: src/modules/platform/inbox/inbox.service.ts
191
+ slot: be.platform
192
+ template: idempotency/platform/inbox/inbox.service.ts.tpl
193
+ - path: src/modules/platform/inbox/inbox.service.spec.ts
194
+ slot: be.platform
195
+ template: idempotency/platform/inbox/inbox.service.spec.ts.tpl
196
+ - path: src/modules/platform/inbox/request-fingerprint.policy.ts
197
+ slot: be.platform
198
+ template: idempotency/platform/inbox/request-fingerprint.policy.ts.tpl
199
+ - path: src/modules/platform/inbox/request-fingerprint.policy.spec.ts
200
+ slot: be.platform
201
+ template: idempotency/platform/inbox/request-fingerprint.policy.spec.ts.tpl
202
+ - path: src/modules/platform/inbox/client-operation.policy.ts
203
+ slot: be.platform
204
+ template: idempotency/platform/inbox/client-operation.policy.ts.tpl
205
+ - path: src/modules/platform/inbox/client-operation.policy.spec.ts
206
+ slot: be.platform
207
+ template: idempotency/platform/inbox/client-operation.policy.spec.ts.tpl
208
+ - path: src/modules/platform/inbox/errors/inbox.error.ts
209
+ slot: be.platform
210
+ template: idempotency/platform/inbox/errors/inbox.error.ts.tpl
211
+ - path: src/modules/platform/inbox/messages/inbox.messages.ts
212
+ slot: be.domain.messages
213
+ template: idempotency/platform/inbox/messages/inbox.messages.ts.tpl
214
+ - path: src/modules/platform/inbox/persistence/connection.ts
215
+ slot: be.persistence
216
+ template: idempotency/platform/inbox/persistence/connection.ts.tpl
217
+ - path: src/modules/platform/inbox/persistence/inbox.sql.ts
218
+ slot: be.persistence
219
+ template: idempotency/platform/inbox/persistence/inbox.sql.ts.tpl
220
+ - path: src/modules/platform/inbox/persistence/inbox.rows.ts
221
+ slot: be.persistence
222
+ template: idempotency/platform/inbox/persistence/inbox.rows.ts.tpl
223
+ - path: src/modules/platform/inbox/client-outcome-backfill.client.ts
224
+ slot: be.platform
225
+ template: idempotency/platform/inbox/client-outcome-backfill.client.ts.tpl
226
+ - path: src/modules/platform/inbox/client-outcome-backfill.client.spec.ts
227
+ slot: be.platform
228
+ template: idempotency/platform/inbox/client-outcome-backfill.client.spec.ts.tpl
229
+ - path: src/modules/platform/inbox/delivery-claim-backfill.client.ts
230
+ slot: be.platform
231
+ template: idempotency/platform/inbox/delivery-claim-backfill.client.ts.tpl
232
+ - path: src/modules/platform/inbox/delivery-claim-backfill.client.spec.ts
233
+ slot: be.platform
234
+ template: idempotency/platform/inbox/delivery-claim-backfill.client.spec.ts.tpl
235
+ - path: src/modules/platform/inbox/delivery-claim-release.client.ts
236
+ slot: be.platform
237
+ template: idempotency/platform/inbox/delivery-claim-release.client.ts.tpl
238
+ - path: src/modules/platform/inbox/delivery-claim-release.client.spec.ts
239
+ slot: be.platform
240
+ template: idempotency/platform/inbox/delivery-claim-release.client.spec.ts.tpl
241
+ - path: src/modules/platform/inbox/persistence/entities/inbox-claim.entity.ts
242
+ slot: be.persistence
243
+ template: idempotency/platform/inbox/persistence/entities/inbox-claim.entity.ts.tpl
244
+ - path: src/modules/platform/inbox/persistence/migrations/1789800005000-create-inbox-claims.ts
245
+ slot: be.persistence
246
+ template: idempotency/platform/inbox/persistence/migrations/1789800005000-create-inbox-claims.ts.tpl
247
+ - path: src/modules/platform/inbox/persistence/migrations/1792800000000-client-operation-outcomes.ts
248
+ slot: be.persistence
249
+ template: idempotency/platform/inbox/persistence/migrations/1792800000000-client-operation-outcomes.ts.tpl
250
+ liteAdapter:
251
+ summary: >-
252
+ Lite keeps the Inbox port and transaction-owned client receipt API. Its existing public.inbox
253
+ table uses Supabase migration authority; the be/inbox platform templates carry the lite adapter and the
254
+ platform/client-operation-outcomes.sql.tpl forward migration. The files tree above describes the full TypeORM adapter.
255
+ Delivery-only webhook scaffolds do not install a client marker. When adopting this client pattern, add the shared
256
+ idempotency/platform/inbox/effectful.decorators.ts.tpl as src/modules/platform/inbox/effectful.decorators.ts and
257
+ export Effectful from the inbox barrel. Every effectful client command then uses that marker and declares the
258
+ idempotency pattern with its required proof scenarios.
259
+ ports:
260
+ - {port: Inbox, adapter: PostgresInbox through the caller's active EntityManager transaction}
261
+ - {port: receipt retention, adapter: a fenced job delivered by the BullMQ queue on the declared Redis and scheduled by a pg_cron tick that enqueues through the queue outbox}
262
+ proof: >-
263
+ The same inbox and fingerprint unit specs apply; declaring idempotency requires both client receipt scenarios in
264
+ lite's local Supabase integration lane.
265
+ see: [knowledge/patterns/be/queues.yaml]
@@ -14,7 +14,7 @@ family: be
14
14
  slots: [be.feature.jobs, be.feature.jobs.steps, be.platform.jobs]
15
15
  pattern: fenced-job
16
16
  provenance:
17
- reviewedAt: 2026-10-02
17
+ reviewedAt: 2026-10-24
18
18
  sources:
19
19
  - knowledge/hfs/README.md
20
20
  - knowledge/hfs/slots.yaml
@@ -40,13 +40,15 @@ rules:
40
40
  delivery (the job key is the BullMQ job id), then `process`, then the guarded `complete`. Nothing else writes the row: no
41
41
  `manager.update`, `increment`, `save`, `insert`, `delete` or query-builder write on the job entity outside `platform/jobs`. The table
42
42
  exists on every connection that runs jobs.
43
+ `ClaimParams.correlationId` comes from the queue delivery and is carried into `ClaimedJob.correlationId` for the processor
44
+ and steps. It keeps the delivery identity across claims, independently of the fencing token and provider run key.
43
45
  rationale: |-
44
46
  A read-modify-write claim races, and a second writer defeats the fence. One owner and one atomic statement keep the token
45
47
  monotonic.
46
48
  cases:
47
49
  - id: case-1
48
50
  when: A worker picks up a job
49
- write: 'const job = await this.claims.claim({ kind: "send-receipt", jobKey: delivery.id, payload, workerId, leaseMs }) // token = previous + 1'
51
+ write: 'const job = await this.claims.claim({ kind: "send-receipt", jobKey: delivery.id, correlationId: delivery.correlationId, payload: delivery.payload, workerId, leaseMs }) // token = previous + 1'
50
52
  verification:
51
53
  automated:
52
54
  - BE_JOB_WRITE_OUTSIDE_OWNER
@@ -81,7 +83,7 @@ rules:
81
83
  hfsRules: [R159]
82
84
  requirement: |-
83
85
  `JobClaims.runKey(job, step)` returns a `RunKey` (`<jobId>:<step>:<fencingToken>`); it is the only way to make one. The step's
84
- one delegating call hands the payload as delivered and the run key to the module service that owns the effect, and every
86
+ one delegating call hands the payload as delivered, the run key and `job.correlationId` to the module service that owns the effect, and every
85
87
  call that reaches an integration passes the key as the provider's idempotency key, so a repeated effect of a stale
86
88
  worker is dropped by the provider. A read from an integration belongs in the application layer, not in a step. When the
87
89
  provider cannot enforce idempotency, constrain overlap and reconcile before a retry.
@@ -89,8 +91,8 @@ rules:
89
91
  Fencing the local row does not fence the remote effect. The key ties the effect to one claim.
90
92
  cases:
91
93
  - id: case-1
92
- when: A step sends a mail
93
- write: 'await this.mail.send(message, this.claims.runKey(job, "send"))'
94
+ when: A step archives a receipt
95
+ write: 'await this.receipts.archiveReceipt(job.payload, this.claims.runKey(job, "store"), job.correlationId)'
94
96
  verification:
95
97
  automated:
96
98
  - BE_JOB_RUN_KEY
@@ -106,7 +108,7 @@ rules:
106
108
  settles through the guarded writes); `steps/<step>.step.ts` declares a class implementing `JobStep`; `application/` holds
107
109
  plain CQRS. The processor and every step are thin feature files (R203 `BE_FEATURE_THIN`): they map the claimed job and
108
110
  delegate into `modules/`; the claim, the fence, the settle and the retry live in `platform/jobs`. A domain service starts
109
- the job with a typed queue producer `enqueueX(payload, tx)` inside its transaction
111
+ the job with a typed queue producer `enqueueX(payload, tx, correlationId)` inside its transaction
110
112
  (queues.yaml). The jobs kind imports only `modules/*`: it never imports an api, saga or reactor feature, and it talks to
111
113
  them through events. An app of kind `worker` or `api` composes the job queue module.
112
114
  rationale: |-
@@ -169,7 +171,7 @@ files:
169
171
  template: jobs/platform/jobs.decorators.ts.tpl
170
172
  - path: src/modules/platform/jobs/jobs.contracts.ts
171
173
  slot: be.platform.jobs
172
- role: "ClaimedJob, GuardedWrite (the token is required), RunKey."
174
+ role: "ClaimedJob with the delivery correlationId, GuardedWrite (the token is required), RunKey."
173
175
  template: jobs/platform/jobs.contracts.ts.tpl
174
176
  - path: src/modules/platform/jobs/jobs.port.ts
175
177
  slot: be.platform.jobs
@@ -259,7 +261,7 @@ files:
259
261
  - path: src/features/jobs/<job>/steps/<step>.step.ts
260
262
  slot: be.feature.jobs.steps
261
263
  template: jobs/job.step.ts.tpl
262
- role: 'One external effect; implements JobStep — thin (R203 `BE_FEATURE_THIN`): the fence call first, then one delegating call that hands the payload and the run key to a service of the modules; the payload guards live there.'
264
+ role: 'One external effect; implements JobStep — thin (R203 `BE_FEATURE_THIN`): the fence call first, then one delegating call that hands the payload, the run key and correlationId to a service of the modules; the payload guards live there.'
263
265
  rules: [BE_JOB_SHAPE, BE_JOB_RUN_KEY]
264
266
  - path: src/features/jobs/<job>/application/<action>.command.ts
265
267
  slot: be.feature.application
@@ -279,11 +281,14 @@ files:
279
281
  liteAdapter:
280
282
  summary: >-
281
283
  Under lite a fenced job is unchanged: the job row with its fencing token, the atomic claim, guarded writes and run keys. The
282
- row's table is a supabase migration, and the processor is fed by the lite queue (pgmq).
284
+ row's table is a supabase migration, and the processor is fed by full's BullMQ queue on the declared Redis, so a lite app
285
+ that declares fenced-job declares `backing: [redis]`. Without a declared Redis a lite app has no fenced job; a long task is
286
+ a declared replicated-job running a cli command.
283
287
  ports:
284
288
  - {port: job claim (platform/jobs), adapter: "the same claim SQL through the shared EntityManager; the table comes from supabase/migrations"}
285
- - {port: processor delivery, adapter: "the lite queue worker (pgmq.read, visibility timeout, archive on dead letter)"}
289
+ - {port: processor delivery, adapter: "full's BullMQ worker (attempts and exponential backoff from the queue policy, failed jobs kept as the dead letter)"}
286
290
  proof: >-
287
291
  Unit specs as full (job-claim.service.spec.ts); declaring fenced-job requires integration specs titled
288
- `fenced-job/<scenario>: ...` for every declared scenario on `starci supabase start` (R284).
289
- see: [knowledge/patterns/be/supabase-queues.yaml]
292
+ `fenced-job/<scenario>: ...` for every declared scenario on the local Supabase stack (R284, which runs no container of its
293
+ own): the claim, the fencing token and the guarded writes are Postgres facts, driven there by the real runner without BullMQ.
294
+ see: [knowledge/patterns/be/queues.yaml]
@@ -181,10 +181,10 @@ liteAdapter:
181
181
  summary: >-
182
182
  Under lite a projection is unchanged in behaviour (only it writes its table, recompute is idempotent and replayable, readers
183
183
  use get*); its table is a supabase migration with a rows type, so the slot carries no projection entity (be.projections lite
184
- overlay), and it is kept current by a reactor or a job on the lite transport.
184
+ overlay), and it is kept current by a reactor on the lite event bus or by a job on the queue (BullMQ on the declared Redis).
185
185
  ports:
186
186
  - {port: read model table, adapter: "a supabase migration plus <name>.sql.ts and <name>.rows.ts under persistence/"}
187
187
  proof: >-
188
188
  Unit specs as full (the projection is a logic role at 100 per file); declaring projection requires integration specs titled
189
189
  `projection/<scenario>: ...` for every declared scenario on `starci supabase start` (R284).
190
- see: [knowledge/patterns/be/supabase-queues.yaml]
190
+ see: [knowledge/patterns/be/event-bus.yaml, knowledge/patterns/be/queues.yaml]
@@ -3,7 +3,7 @@ id: be.queues
3
3
  title: Queues and schedulers
4
4
  purpose: |-
5
5
  Given work that must run later, in the background or on a schedule, how is it started? It is a job of a typed queue:
6
- `modules/queues/<queue>/<queue>.queue.ts` is the producer (`enqueueX(payload, tx)` writes the outbox in the caller's
6
+ `modules/queues/<queue>/<queue>.queue.ts` is the producer (`enqueueX(payload, tx, correlationId)` writes the outbox in the caller's
7
7
  transaction), `platform/queue` relays committed rows to BullMQ (the job id is the outbox id, so a relay retry never duplicates
8
8
  a job), and a `features/jobs/<job>/<job>.processor.ts` runs it. A schedule is a BullMQ job scheduler declared beside the
9
9
  producer; `@Cron`, `@Interval` and `@Timeout` do not exist.
@@ -15,12 +15,13 @@ family: be
15
15
  slots: [be.feature.jobs, be.queues, be.platform.queue]
16
16
  pattern: queue
17
17
  provenance:
18
- reviewedAt: 2026-10-02
18
+ reviewedAt: 2026-10-24
19
19
  sources:
20
20
  - knowledge/hfs/README.md
21
21
  - knowledge/hfs/slots.yaml
22
22
  - knowledge/hfs/rules.yaml
23
23
  - owner decision 2026-10-02 (BullMQ queues with a transactional outbox, schedulers replace cron)
24
+ - ruling:R212/case-10 (2026-10-24; the queue is Redis in both editions, lite declares its Redis)
24
25
  - https://docs.bullmq.io/guide/job-schedulers
25
26
  limitations: |-
26
27
  Static checks prove who enqueues, with which transaction, and that nothing imports BullMQ or Nest scheduling outside the
@@ -33,17 +34,18 @@ rules:
33
34
  hfsRules: [R152, R154]
34
35
  requirement: |-
35
36
  Each queue is a folder `be/src/modules/queues/<queue>/` with `<queue>.queue.ts` (a class whose methods are the typed
36
- producers `enqueueX(payload, tx: EntityManager)`), optional `<queue>.contracts.ts` (payload types and the queue name) and
37
- `index.ts`. A producer writes the `QueueOutbox` port of `platform/queue` with its own `tx` as the first argument. A producer
37
+ producers `enqueueX(payload, tx: EntityManager, correlationId: string)`), optional `<queue>.contracts.ts` (payload types and the queue name) and
38
+ `index.ts`. A producer calls `QueueOutbox.write(tx, queue, payload, correlationId)` in `platform/queue`. A producer
38
39
  is called only by a domain service, inside its transaction (the same rule as `eventBus.publish`). Work inside one context
39
40
  that must happen now is a direct call, not a job.
41
+ Current outbox writes validate the explicit correlation id before issuing SQL.
40
42
  rationale: |-
41
43
  The job row commits with the change that caused it, so a rolled-back change never starts work and a committed one is never
42
44
  lost.
43
45
  cases:
44
46
  - id: case-1
45
47
  when: A domain service must send a receipt after payment
46
- write: 'await this.entityManager.transaction(async (manager) => { await manager.save(...); await this.receiptQueue.enqueueSendReceipt({ orderId }, manager) })'
48
+ write: 'await this.entityManager.transaction(async (manager) => { await manager.save(...); await this.receiptQueue.enqueueSendReceipt({ orderId }, manager, request.correlationId) })'
47
49
  verification:
48
50
  automated:
49
51
  - BE_OUTBOX_WRITE_TX
@@ -60,34 +62,47 @@ rules:
60
62
  registered connection and adds each job with `jobId = <outbox id>`; BullMQ ignores a second add of the same id, so a relay
61
63
  that crashes after adding and before marking the row sent re-adds harmlessly. Nothing else constructs a `Queue`, `Worker`
62
64
  or `FlowProducer`.
65
+ The row's explicit `correlationId` travels in the job envelope and `QueueDelivery`, following BE-LOGGING-4.
66
+ BullMQ envelopes carry the owned `schema: "starci/queue@1"` marker; enqueued jobs wrap the payload and explicit id.
67
+ Only the owned marker family is interpreted as transport data. Unmarked legacy objects remain whole business payloads,
68
+ including their `kind`, `schema` and `correlationId` fields.
63
69
  rationale: |-
64
70
  The relay is at least once; a deterministic job id turns it into effectively once.
65
71
  cases:
66
72
  - id: case-1
67
73
  when: The relay re-reads a row it already added
68
- write: 'queue.add(name, payload, { jobId: row.id }) is a no-op for an id BullMQ already holds.'
74
+ write: 'transport.add(row.queue, row.id, row.payload, row.correlationId) preserves the outbox id and correlation id; the BullMQ adapter adds the envelope with jobId = row.id.'
69
75
  verification:
70
76
  automated:
71
77
  - BE_INFRA_OWNER
72
78
  manual: []
73
79
  relatedExamples: [queue]
74
80
  - id: BE-QUEUES-3
75
- title: A schedule is a BullMQ job scheduler declared with its queue
81
+ title: A schedule is a BullMQ job scheduler declared with its queue, or a pg_cron tick that writes the outbox
76
82
  kind: mandatory
77
- hfsRules: [R90]
83
+ hfsRules: [R90, R258]
78
84
  requirement: |-
79
85
  A recurring task is a job scheduler (`upsertJobScheduler`) that the queue module declares in its `schedulers` list and
80
- `platform/queue` registers at boot; its job is processed by the same `features/jobs/<job>/<job>.processor.ts` as any
81
- enqueued job. `@nestjs/schedule`, `@Cron`, `@Interval`, `@Timeout` and `setInterval` for work are not used anywhere.
86
+ `platform/queue` registers at boot, or, in an app on Supabase, a uniquely named pg_cron job of a migration whose command
87
+ (directly or through one `*_tick` function) only inserts one row of a queue constant into the queue outbox table of
88
+ `ruleParams.db.queueOutbox` (`public.queue`), which the relay hands to BullMQ; a tick has exactly one of the two owners. Its job is processed by the same
89
+ `features/jobs/<job>/<job>.processor.ts` as any enqueued job. `@nestjs/schedule`, `@Cron`, `@Interval`, `@Timeout` and
90
+ `setInterval` for work are not used anywhere.
82
91
  rationale: |-
83
- One scheduler in Redis fires once for the whole deployment, where cron in a replicated process fires once per replica.
92
+ One scheduler in Redis, or one pg_cron job in the database, fires once for the whole deployment, where cron in a replicated
93
+ process fires once per replica.
84
94
  cases:
85
95
  - id: case-1
86
96
  when: Expire unpaid orders every five minutes
87
97
  write: 'order-expiry.queue.ts declares scheduler { every: 5 minutes } for the expire-orders job; the processor dispatches ExpireOrdersCommand.'
98
+ - id: case-2
99
+ when: A lite app on Supabase sends booking reminders every fifteen minutes
100
+ write: "A migration declares private.booking_reminder_tick() as one insert into public.queue (queue, payload, created_at) values ('booking_reminder', jsonb_build_object('tick', true), now()) and schedules it with cron.schedule('booking_reminder_tick', '*/15 * * * *', ...); the send-reminder processor is the queue's."
88
101
  verification:
89
102
  automated:
90
103
  - BE_INFRA_OWNER
104
+ - DB_PGMQ_DECLARED
105
+ - DB_CRON_OWNED
91
106
  manual:
92
107
  - Confirm the cadence and the job's idempotence.
93
108
  relatedExamples: [queue]
@@ -169,6 +184,13 @@ files:
169
184
  - path: src/modules/platform/queue/queue.log-events.ts
170
185
  slot: be.platform.queue
171
186
  template: queues/platform/queue.log-events.ts.tpl
187
+ - path: src/modules/platform/queue/errors/queue.error.ts
188
+ slot: be.platform.queue
189
+ role: "Malformed stored queue envelope failures."
190
+ template: queues/platform/errors/queue.error.ts.tpl
191
+ - path: src/modules/platform/queue/messages/queue.messages.ts
192
+ slot: be.domain.messages
193
+ template: queues/platform/messages/queue.messages.ts.tpl
172
194
  - path: src/modules/platform/queue/bullmq-queue-transport.client.ts
173
195
  slot: be.platform.queue
174
196
  role: "The one bullmq importer."
@@ -242,18 +264,31 @@ files:
242
264
  role: "Titles queue/<scenario>: ... prove the pattern."
243
265
  spec: queue/enqueue-atomic-with-transaction, queue/relay-job-id-is-outbox-id, queue/scheduler-fires-once
244
266
  rules: [BE_PATTERN_SPEC_MISSING]
245
- # Edition lite (R212, owner ruling 2026-10-14): the same ports and rules, a Supabase adapter behind them.
267
+ # Edition lite (ruling:R212/case-10): the queue is BullMQ on Redis in both editions; only the outbox table's schema authority differs.
246
268
  liteAdapter:
247
269
  summary: >-
248
- Under lite the queue pattern is unchanged above the port (producers, the outbox written in the caller's transaction, processors,
249
- schedulers declared with their queue) and its transport is Supabase Queues (pgmq) instead of BullMQ and Redis.
270
+ Under lite the queue pattern is full's, adapter included: producers write the outbox in the caller's transaction, the relay
271
+ adds each committed row to BullMQ with the outbox id as the job id, BullMQ workers run the processors with the queue policy's
272
+ attempts and exponential backoff, and a job that exhausts its attempts stays in BullMQ's failed set (the dead letter). The
273
+ queue needs Redis: a lite app that declares queue or fenced-job declares `backing: [redis]` in hfs.json sides.be, and its
274
+ stack runs one Redis with AOF persistence (appendonly yes, appendfsync everysec) on a named volume. One difference from
275
+ full's adapter: the relay marks an outbox row sent only after the transport's persist step confirms Redis wrote the job to
276
+ its append-only file (WAITAOF) and still holds it, so a job Redis loses inside the at most one second fsync window leaves
277
+ its row waiting and is added again; that is why a lite queue does not lose a job. Without a declared Redis there is no queue
278
+ pattern: the work is a direct call inside the request, or a declared replicated-job running a cli command. Supabase Queues
279
+ (pgmq) is not a queue transport. `starci app add queue|job` writes this adapter in a lite app, with the outbox and job
280
+ tables as one supabase migration, the Redis declaration and the persisted Redis of every docker-swarm environment.
250
281
  ports:
251
- - {port: QueueOutbox (platform/queue), adapter: the same outbox row written through the caller's tx; nothing changes}
252
- - {port: queue transport (queue-transport.port.ts), adapter: "pgmq-queue-transport.client.ts: pgmq.send through the shared EntityManager, the outbox id inside the message"}
253
- - {port: QueueWorkerRegistry (platform/queue), adapter: "a poll loop per queue: pgmq.read with the policy's visibility timeout, pgmq.delete on success, pgmq.set_vt for backoff, pgmq.archive when read_ct exceeds the attempts"}
254
- - {port: schedulers, adapter: "a pg_cron job declared in a supabase migration that calls pgmq.send; no BullMQ job scheduler"}
255
- forbids: [bullmq, "@nestjs/bullmq", redis, ioredis, "@nestjs/schedule"]
282
+ - {port: QueueOutbox (platform/queue), adapter: "full's outbox row written through the caller's tx; the table comes from a supabase migration (no TypeORM entity under lite)"}
283
+ - {port: queue transport (queue-transport.port.ts), adapter: "full's bullmq-queue-transport.client.ts over the declared Redis (host, port, db and the password read from its sealed secret file), plus persist(jobs): WAITAOF 1 0 on each queue's connection, then each job's state, refusing with QUEUE_NOT_PERSISTED or QUEUE_JOB_LOST (errors/queue.error.ts)"}
284
+ - {port: outbox relay (queue-relay.service.ts), adapter: "full's relay, which calls persist after the adds and before it marks the rows sent, so a refusal rolls the mark back and the next pass adds the rows again"}
285
+ - {port: QueueWorkerRegistry (platform/queue), adapter: "full's BullMQ worker per queue: attempts and exponential backoff from queue.policy.ts, failed jobs kept as the dead letter"}
286
+ - {port: schedulers, adapter: "a BullMQ job scheduler declared with its queue, as full, or a pg_cron job in a supabase migration whose tick inserts one row of the queue constant into the queue outbox"}
287
+ forbids: [kafkajs, "@nestjs/schedule"]
256
288
  proof: >-
257
- Unit specs as full (queue-outbox, relay and worker services, the pgmq client against mockEntityManager); declaring queue
258
- requires integration specs titled `queue/<scenario>: ...` for every declared scenario on `starci supabase start` (R284).
259
- see: [knowledge/patterns/be/supabase-queues.yaml]
289
+ Unit specs as full (queue-outbox, relay and worker services, the BullMQ client against a mocked factory); declaring queue
290
+ requires integration specs titled `queue/<scenario>: ...` for every declared scenario on the local Supabase stack (R284,
291
+ which runs no container of its own): the outbox, its commit boundary, the relay identity and the pg_cron tick are Postgres
292
+ facts proved there with the real outbox writer and relay, and the queue transport records what the relay hands to BullMQ
293
+ (the job id is the outbox id). BullMQ itself is covered by the unit spec of the BullMQ client, which also proves the
294
+ persist refusals; the relay's unit spec proves a refused persist leaves the rows waiting.