@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
@@ -1,8 +1,8 @@
1
1
  import { Test } from "@nestjs/testing"
2
2
  import { FakeClock, mock } from "@starci/jest-preset"
3
3
  import { CLOCK } from "@modules/platform/clock"
4
- import { LOGGER } from "@modules/platform/logging"
5
- import type { Logger } from "@modules/platform/logging"
4
+ import { CORRELATION_RUNNER, LOGGER } from "@modules/platform/logging"
5
+ import type { CorrelationRunner, Logger } from "@modules/platform/logging"
6
6
  import { BaseEvent } from "./event-bus.contracts"
7
7
  import type { ParsedEvent } from "./event-bus.contracts"
8
8
  import { EVENT_BUS_OPTIONS, EVENT_TRANSPORT } from "./event-bus.decorators"
@@ -13,8 +13,6 @@ import { readEnvelope } from "./event-envelope.policy"
13
13
  import { EventRunnerService } from "./event-runner.service"
14
14
  import type { EventSubscription, EventTransport, InboundMessage } from "./event-transport.port"
15
15
 
16
- /** Asymmetric matchers read as `unknown`: jest types them `any`, which may not flow into a property or a typed argument. */
17
- const anyOf = (type: unknown): unknown => expect.any(type)
18
16
  const objectLike = (shape: Record<string, unknown>): unknown => expect.objectContaining(shape)
19
17
 
20
18
  const AT = "2026-02-03T04:05:06.000Z"
@@ -60,7 +58,12 @@ const message = (overrides: Partial<InboundMessage> = {}): InboundMessage => ({
60
58
  partition: 0,
61
59
  offset: "4",
62
60
  key: "o-1",
63
- value: JSON.stringify({ eventId: "o-1", eventName: "probe.ping", payload: { ref: "o-1" } }),
61
+ value: JSON.stringify({
62
+ metadata: { correlationId: "request-1" },
63
+ eventId: "o-1",
64
+ eventName: "probe.ping",
65
+ payload: { ref: "o-1" },
66
+ }),
64
67
  headers: {},
65
68
  ...overrides,
66
69
  })
@@ -68,6 +71,8 @@ const message = (overrides: Partial<InboundMessage> = {}): InboundMessage => ({
68
71
  const build = async (register = true) => {
69
72
  const transport = mock<EventTransport>()
70
73
  const logger = mock<Logger>()
74
+ const correlation = mock<CorrelationRunner>()
75
+ correlation.run.mockImplementation((_id, work) => work())
71
76
  const handle = jest.fn<Promise<void>, Array<never>>()
72
77
  const subscriptions: Array<EventSubscription> = []
73
78
  transport.subscribe.mockImplementation((subscription) => {
@@ -81,6 +86,7 @@ const build = async (register = true) => {
81
86
  { provide: EVENT_TRANSPORT, useValue: transport },
82
87
  { provide: CLOCK, useValue: new FakeClock(AT) },
83
88
  { provide: LOGGER, useValue: logger },
89
+ { provide: CORRELATION_RUNNER, useValue: correlation },
84
90
  ],
85
91
  }).compile()
86
92
  const runner = moduleRef.get(EventRunnerService)
@@ -88,202 +94,299 @@ const build = async (register = true) => {
88
94
  const consumer: EventConsumer<PingEvent> = { event: PingEvent, handle }
89
95
  runner.add(consumer)
90
96
  }
91
- return { runner, transport, logger, handle, subscriptions }
97
+ return { runner, transport, logger, handle, subscriptions, correlation }
92
98
  }
93
99
 
94
- describe("EventRunnerService", () => {
95
- describe("onApplicationBootstrap", () => {
96
- it("subscribes to the main and retry topic of every registered event", async () => {
97
- const { runner, subscriptions } = await build()
100
+ describe("EventRunnerService onApplicationBootstrap", () => {
101
+ it("subscribes to the main and retry topic of every registered event", async () => {
102
+ const { runner, subscriptions } = await build()
98
103
 
99
- await runner.onApplicationBootstrap()
104
+ await runner.onApplicationBootstrap()
100
105
 
101
- expect(subscriptions.map((subscription) => subscription.topics)).toEqual([
102
- ["run-1.events.probe", "run-1.events.probe.retry"],
103
- ])
104
- })
106
+ expect(subscriptions.map((subscription) => subscription.topics)).toEqual([
107
+ ["run-1.events.probe", "run-1.events.probe.retry"],
108
+ ])
109
+ })
105
110
 
106
- it("subscribes to nothing when no consumer registered", async () => {
107
- const { runner, transport } = await build(false)
111
+ it("subscribes to nothing when no consumer registered", async () => {
112
+ const { runner, transport } = await build(false)
108
113
 
109
- await runner.onApplicationBootstrap()
114
+ await runner.onApplicationBootstrap()
110
115
 
111
- expect(transport.subscribe).not.toHaveBeenCalled()
112
- })
116
+ expect(transport.subscribe).not.toHaveBeenCalled()
117
+ })
118
+
119
+ it("hands what the transport delivers to receive", async () => {
120
+ const { runner, subscriptions, handle } = await build()
121
+ handle.mockResolvedValue(undefined)
122
+ await runner.onApplicationBootstrap()
113
123
 
114
- it("hands what the transport delivers to receive", async () => {
115
- const { runner, subscriptions, handle } = await build()
116
- handle.mockResolvedValue(undefined)
117
- await runner.onApplicationBootstrap()
124
+ await subscriptions[0]?.onMessage(message())
118
125
 
119
- await subscriptions[0]?.onMessage(message())
126
+ expect(handle).toHaveBeenCalledTimes(1)
127
+ })
128
+ })
120
129
 
121
- expect(handle).toHaveBeenCalledTimes(1)
130
+ describe("EventRunnerService correlation scope", () => {
131
+ it("restores the explicit correlation context around consumer work", async () => {
132
+ const { runner, handle, correlation } = await build()
133
+ let active = ""
134
+ correlation.run.mockImplementation((id, work) => {
135
+ active = id
136
+ return work()
137
+ })
138
+ handle.mockImplementation(() => {
139
+ expect(active).toBe("request-1")
140
+ return Promise.resolve()
122
141
  })
142
+
143
+ await runner.receive(message())
144
+
145
+ expect(handle).toHaveBeenCalledWith(objectLike({ correlationId: "request-1" }))
146
+ expect(correlation.run).toHaveBeenCalledTimes(1)
123
147
  })
148
+ })
124
149
 
125
- describe("receive", () => {
126
- it("hands the parsed event to its consumer with the event id and the first attempt", async () => {
127
- const { runner, handle, transport } = await build()
128
- handle.mockResolvedValue(undefined)
150
+ describe("EventRunnerService legacy broker envelopes", () => {
151
+ it("delivers a legacy broker envelope with an explicit root at its event id", async () => {
152
+ const { runner, handle } = await build()
153
+ const legacy = { eventId: "legacy-1", eventName: "probe.ping", payload: { ref: "order-1" } }
129
154
 
130
- await runner.receive(message())
155
+ await runner.receive(message({ value: JSON.stringify(legacy) }))
131
156
 
132
- expect(handle).toHaveBeenCalledWith({
133
- eventId: "o-1",
134
- event: new PingEvent("o-1", { ref: "o-1" }),
135
- attempt: 1,
136
- })
137
- expect(transport.send).not.toHaveBeenCalled()
157
+ expect(handle).toHaveBeenCalledWith({
158
+ correlationId: "legacy-1",
159
+ eventId: "legacy-1",
160
+ event: new PingEvent("legacy-1", { ref: "order-1" }),
161
+ attempt: 1,
138
162
  })
163
+ })
139
164
 
140
- it("hands one event to every consumer registered for it and retries it once when any of them failed", async () => {
141
- const { runner, handle, transport } = await build()
142
- const second = jest.fn<Promise<void>, Array<never>>()
143
- const third = jest.fn<Promise<void>, Array<never>>()
144
- handle.mockRejectedValue(new Error("the first consumer failed"))
145
- second.mockResolvedValue(undefined)
146
- third.mockRejectedValue(new Error("the third consumer failed too"))
147
- runner.add({ event: PingEvent, handle: second })
148
- runner.add({ event: PingEvent, handle: third })
149
-
150
- await runner.receive(message())
151
-
152
- expect(second).toHaveBeenCalledTimes(1)
153
- expect(third).toHaveBeenCalledTimes(1)
154
- expect(transport.send).toHaveBeenCalledTimes(1)
155
- })
165
+ it.each([
166
+ { attempt: "2", topic: "run-1.events.probe.retry" },
167
+ { attempt: "5", topic: "run-1.events.probe.dlq" },
168
+ ])("promotes a legacy body before sending to $topic", async ({ attempt, topic }) => {
169
+ const { runner, handle, transport } = await build()
170
+ handle.mockRejectedValue(new Error("provider unavailable"))
171
+ const legacy = {
172
+ eventId: "legacy-1",
173
+ eventName: "probe.ping",
174
+ payload: { ref: "order-1", businessField: "retained" },
175
+ causeId: "preceding-event",
176
+ }
177
+
178
+ await runner.receive(message({ value: JSON.stringify(legacy), headers: { attempt } }))
179
+
180
+ expect(transport.send).toHaveBeenCalledWith([objectLike({
181
+ topic,
182
+ value: JSON.stringify({ ...legacy, metadata: { correlationId: "legacy-1" } }),
183
+ })])
184
+ expect(handle).toHaveBeenCalledWith(objectLike({ correlationId: "legacy-1" }))
185
+ })
186
+ })
156
187
 
157
- it("leaves an event nobody here consumes alone", async () => {
158
- const { runner, handle, transport, logger } = await build()
188
+ describe("EventRunnerService receive consumer delivery", () => {
189
+ it("hands the parsed event to its consumer with the event id and the first attempt", async () => {
190
+ const { runner, handle, transport } = await build()
191
+ handle.mockResolvedValue(undefined)
159
192
 
160
- await runner.receive(
161
- message({ value: JSON.stringify({ eventId: "x", eventName: "probe.other", payload: {} }) }),
162
- )
193
+ await runner.receive(message())
163
194
 
164
- expect(handle).not.toHaveBeenCalled()
165
- expect(transport.send).not.toHaveBeenCalled()
166
- expect(logger.warn).not.toHaveBeenCalled()
195
+ expect(handle).toHaveBeenCalledWith({
196
+ correlationId: "request-1",
197
+ eventId: "o-1",
198
+ event: new PingEvent("o-1", { ref: "o-1" }),
199
+ attempt: 1,
167
200
  })
201
+ expect(transport.send).not.toHaveBeenCalled()
202
+ })
168
203
 
169
- it("logs and skips a value that is not an envelope", async () => {
170
- const { runner, handle, logger } = await build()
171
-
172
- await runner.receive(message({ value: "not json" }))
173
- await runner.receive(message({ value: JSON.stringify([1]) }))
204
+ it("hands one event to every consumer registered for it and retries it once when any of them failed", async () => {
205
+ const { runner, handle, transport } = await build()
206
+ const second = jest.fn<Promise<void>, Array<never>>()
207
+ const third = jest.fn<Promise<void>, Array<never>>()
208
+ handle.mockRejectedValue(new Error("the first consumer failed"))
209
+ second.mockResolvedValue(undefined)
210
+ third.mockRejectedValue(new Error("the third consumer failed too"))
211
+ runner.add({ event: PingEvent, handle: second })
212
+ runner.add({ event: PingEvent, handle: third })
213
+
214
+ await runner.receive(message())
215
+
216
+ expect(second).toHaveBeenCalledTimes(1)
217
+ expect(third).toHaveBeenCalledTimes(1)
218
+ expect(transport.send).toHaveBeenCalledTimes(1)
219
+ })
220
+ })
174
221
 
175
- expect(handle).not.toHaveBeenCalled()
176
- expect(logger.error).toHaveBeenCalledWith(EventBusLogEvent.MessageSkipped, anyOf(SyntaxError), {
177
- topic: "run-1.events.probe",
178
- })
179
- expect(logger.warn).toHaveBeenCalledTimes(1)
180
- expect(logger.warn).toHaveBeenCalledWith(EventBusLogEvent.MessageSkipped, { topic: "run-1.events.probe" })
181
- })
222
+ describe("EventRunnerService receive unhandled messages", () => {
223
+ it("leaves an event nobody here consumes alone", async () => {
224
+ const { runner, handle, transport, logger } = await build()
225
+
226
+ await runner.receive(
227
+ message({
228
+ value: JSON.stringify({
229
+ metadata: { correlationId: "request-1" },
230
+ eventId: "x",
231
+ eventName: "probe.other",
232
+ payload: {},
233
+ }),
234
+ }),
235
+ )
182
236
 
183
- it("buries an envelope the event class cannot read, without retrying it", async () => {
184
- const { runner, handle, transport } = await build()
185
- const value = JSON.stringify({ eventId: "o-2", eventName: "probe.ping", payload: {} })
186
-
187
- await runner.receive(message({ key: "o-2", value }))
188
-
189
- expect(handle).not.toHaveBeenCalled()
190
- expect(transport.send).toHaveBeenCalledWith([
191
- {
192
- topic: "run-1.events.probe.dlq",
193
- key: "o-2",
194
- value,
195
- headers: {
196
- attempt: "1",
197
- reason: "the envelope does not have the shape of the event",
198
- "origin-topic": "run-1.events.probe",
199
- },
200
- },
201
- ])
202
- })
237
+ expect(handle).not.toHaveBeenCalled()
238
+ expect(transport.send).not.toHaveBeenCalled()
239
+ expect(logger.warn).not.toHaveBeenCalled()
240
+ })
203
241
 
204
- it("buries once and runs no later consumer of the event when an envelope cannot be read", async () => {
205
- const { runner, handle, transport } = await build()
206
- const later = jest.fn<Promise<void>, Array<never>>()
207
- runner.add({ event: PingEvent, handle: later })
208
- const value = JSON.stringify({ eventId: "o-2", eventName: "probe.ping", payload: {} })
242
+ it("logs and skips a value that is not an envelope", async () => {
243
+ const { runner, handle, logger } = await build()
209
244
 
210
- await runner.receive(message({ key: "o-2", value }))
245
+ await runner.receive(message({ value: "not json" }))
246
+ await runner.receive(message({ value: JSON.stringify([1]) }))
211
247
 
212
- expect(handle).not.toHaveBeenCalled()
213
- expect(later).not.toHaveBeenCalled()
214
- expect(transport.send).toHaveBeenCalledTimes(1)
248
+ expect(handle).not.toHaveBeenCalled()
249
+ expect(logger.error).toHaveBeenCalledWith(EventBusLogEvent.MessageSkipped, expect.any(SyntaxError), {
250
+ topic: "run-1.events.probe",
215
251
  })
252
+ expect(logger.warn).toHaveBeenCalledTimes(1)
253
+ expect(logger.warn).toHaveBeenCalledWith(EventBusLogEvent.MessageSkipped, { topic: "run-1.events.probe" })
254
+ })
255
+ })
216
256
 
217
- it("puts a failed delivery on the retry topic with the next attempt and a doubled backoff", async () => {
218
- const { runner, handle, transport, logger } = await build()
219
- const failure = new Error("the database is down")
220
- handle.mockRejectedValue(failure)
257
+ describe("EventRunnerService receive invalid event payloads", () => {
258
+ it("buries present malformed metadata before calling business code", async () => {
259
+ const { runner, handle, transport } = await build()
260
+ const value = JSON.stringify({ eventId: "o-2", eventName: "probe.ping", payload: { ref: "o-2" }, metadata: null })
221
261
 
222
- await runner.receive(message({ headers: { attempt: "2" } }))
262
+ await runner.receive(message({ value }))
223
263
 
224
- expect(transport.send).toHaveBeenCalledWith([
225
- {
226
- topic: "run-1.events.probe.retry",
227
- key: "o-1",
228
- value: message().value,
229
- headers: { attempt: "3", "not-before": String(NOW + 1000) },
264
+ expect(handle).not.toHaveBeenCalled()
265
+ expect(transport.send).toHaveBeenCalledWith([
266
+ expect.objectContaining({
267
+ topic: "run-1.events.probe.dlq",
268
+ headers: objectLike({ reason: "missing or malformed correlation metadata" }),
269
+ }),
270
+ ])
271
+ })
272
+
273
+ it("buries an envelope the event class cannot read, without retrying it", async () => {
274
+ const { runner, handle, transport } = await build()
275
+ const value = JSON.stringify({
276
+ metadata: { correlationId: "request-1" },
277
+ eventId: "o-2",
278
+ eventName: "probe.ping",
279
+ payload: {},
280
+ })
281
+
282
+ await runner.receive(message({ key: "o-2", value }))
283
+
284
+ expect(handle).not.toHaveBeenCalled()
285
+ expect(transport.send).toHaveBeenCalledWith([
286
+ {
287
+ topic: "run-1.events.probe.dlq",
288
+ key: "o-2",
289
+ value,
290
+ headers: {
291
+ attempt: "1",
292
+ reason: "the envelope does not have the shape of the event",
293
+ "origin-topic": "run-1.events.probe",
230
294
  },
231
- ])
232
- expect(logger.error).toHaveBeenCalledWith(EventBusLogEvent.DeliveryFailed, failure, {
233
- event: "probe.ping",
234
- attempt: 2,
235
- })
236
- expect(logger.warn).toHaveBeenCalledWith(EventBusLogEvent.DeliveryRetried, {
237
- event: "probe.ping",
238
- attempt: 2,
239
- reason: "the database is down",
240
- })
295
+ },
296
+ ])
297
+ })
298
+
299
+ it("buries once and runs no later consumer of the event when an envelope cannot be read", async () => {
300
+ const { runner, handle, transport } = await build()
301
+ const later = jest.fn<Promise<void>, Array<never>>()
302
+ runner.add({ event: PingEvent, handle: later })
303
+ const value = JSON.stringify({
304
+ metadata: { correlationId: "request-1" },
305
+ eventId: "o-2",
306
+ eventName: "probe.ping",
307
+ payload: {},
241
308
  })
242
309
 
243
- it("buries the delivery that ran out of attempts, naming the reason and the topic it came from", async () => {
244
- const { runner, handle, transport, logger } = await build()
245
- handle.mockRejectedValue("a text, not an error")
310
+ await runner.receive(message({ key: "o-2", value }))
311
+
312
+ expect(handle).not.toHaveBeenCalled()
313
+ expect(later).not.toHaveBeenCalled()
314
+ expect(transport.send).toHaveBeenCalledTimes(1)
315
+ })
316
+ })
246
317
 
247
- await runner.receive(message({ headers: { attempt: "5" } }))
318
+ describe("EventRunnerService receive failed deliveries", () => {
319
+ it("puts a failed delivery on the retry topic with the next attempt and a doubled backoff", async () => {
320
+ const { runner, handle, transport, logger } = await build()
321
+ const failure = new Error("the database is down")
322
+ handle.mockRejectedValue(failure)
323
+
324
+ await runner.receive(message({ headers: { attempt: "2" } }))
325
+
326
+ expect(transport.send).toHaveBeenCalledWith([
327
+ {
328
+ topic: "run-1.events.probe.retry",
329
+ key: "o-1",
330
+ value: message().value,
331
+ headers: { attempt: "3", "not-before": String(NOW + 1000) },
332
+ },
333
+ ])
334
+ expect(logger.error).toHaveBeenCalledWith(EventBusLogEvent.DeliveryFailed, failure, {
335
+ event: "probe.ping",
336
+ attempt: 2,
337
+ })
338
+ expect(logger.warn).toHaveBeenCalledWith(EventBusLogEvent.DeliveryRetried, {
339
+ event: "probe.ping",
340
+ attempt: 2,
341
+ reason: "the database is down",
342
+ })
343
+ })
248
344
 
249
- expect(transport.send).toHaveBeenCalledWith([
250
- {
251
- topic: "run-1.events.probe.dlq",
252
- key: "o-1",
253
- value: message().value,
254
- headers: { attempt: "5", reason: "a text, not an error", "origin-topic": "run-1.events.probe" },
255
- },
256
- ])
257
- expect(logger.warn).toHaveBeenCalledWith(EventBusLogEvent.DeliveryBuried, {
258
- event: "probe.ping",
259
- attempts: 5,
260
- reason: "a text, not an error",
261
- })
345
+ it("buries the delivery that ran out of attempts, naming the reason and the topic it came from", async () => {
346
+ const { runner, handle, transport, logger } = await build()
347
+ handle.mockRejectedValue("a text, not an error")
348
+
349
+ await runner.receive(message({ headers: { attempt: "5" } }))
350
+
351
+ expect(transport.send).toHaveBeenCalledWith([
352
+ {
353
+ topic: "run-1.events.probe.dlq",
354
+ key: "o-1",
355
+ value: message().value,
356
+ headers: { attempt: "5", reason: "a text, not an error", "origin-topic": "run-1.events.probe" },
357
+ },
358
+ ])
359
+ expect(logger.warn).toHaveBeenCalledWith(EventBusLogEvent.DeliveryBuried, {
360
+ event: "probe.ping",
361
+ attempts: 5,
362
+ reason: "a text, not an error",
262
363
  })
364
+ })
365
+ })
263
366
 
264
- it("waits for the backoff of a retry message before handing it over", async () => {
265
- const { runner, handle, transport } = await build()
266
- handle.mockResolvedValue(undefined)
367
+ describe("EventRunnerService receive retry timing", () => {
368
+ it("waits for the backoff of a retry message before handing it over", async () => {
369
+ const { runner, handle, transport } = await build()
370
+ handle.mockResolvedValue(undefined)
267
371
 
268
- await runner.receive(
269
- message({
270
- topic: "run-1.events.probe.retry",
271
- headers: { attempt: "2", "not-before": String(NOW + 800) },
272
- }),
273
- )
372
+ await runner.receive(
373
+ message({
374
+ topic: "run-1.events.probe.retry",
375
+ headers: { attempt: "2", "not-before": String(NOW + 800) },
376
+ }),
377
+ )
274
378
 
275
- expect(transport.wait).toHaveBeenCalledWith(800)
276
- expect(handle).toHaveBeenCalledWith(objectLike({ attempt: 2 }))
277
- })
379
+ expect(transport.wait).toHaveBeenCalledWith(800)
380
+ expect(handle).toHaveBeenCalledWith(expect.objectContaining({ attempt: 2 }))
381
+ })
278
382
 
279
- it("does not wait for a retry message whose backoff has passed", async () => {
280
- const { runner, handle, transport } = await build()
281
- handle.mockResolvedValue(undefined)
383
+ it("does not wait for a retry message whose backoff has passed", async () => {
384
+ const { runner, handle, transport } = await build()
385
+ handle.mockResolvedValue(undefined)
282
386
 
283
- await runner.receive(message({ headers: { attempt: "2", "not-before": String(NOW - 5) } }))
387
+ await runner.receive(message({ headers: { attempt: "2", "not-before": String(NOW - 5) } }))
284
388
 
285
- expect(transport.wait).not.toHaveBeenCalled()
286
- expect(handle).toHaveBeenCalledTimes(1)
287
- })
389
+ expect(transport.wait).not.toHaveBeenCalled()
390
+ expect(handle).toHaveBeenCalledTimes(1)
288
391
  })
289
392
  })
@@ -2,8 +2,8 @@ import { Injectable } from "@nestjs/common"
2
2
  import type { OnApplicationBootstrap } from "@nestjs/common"
3
3
  import { InjectClock } from "@modules/platform/clock"
4
4
  import type { Clock } from "@modules/platform/clock"
5
- import { InjectLogger } from "@modules/platform/logging"
6
- import type { Logger } from "@modules/platform/logging"
5
+ import { InjectCorrelationRunner, InjectLogger } from "@modules/platform/logging"
6
+ import type { CorrelationRunner, Logger } from "@modules/platform/logging"
7
7
  import { eachInOrder } from "@modules/platform/primitives"
8
8
  import type { BaseEvent } from "./event-bus.contracts"
9
9
  import { InjectEventBusOptions, InjectEventTransport } from "./event-bus.decorators"
@@ -39,6 +39,7 @@ export class EventRunnerService implements EventConsumerRegistry, OnApplicationB
39
39
  @InjectEventTransport() private readonly transport: EventTransport,
40
40
  @InjectClock() private readonly clock: Clock,
41
41
  @InjectLogger() private readonly logger: Logger,
42
+ @InjectCorrelationRunner() private readonly correlation: CorrelationRunner,
42
43
  ) {}
43
44
 
44
45
  /** Registers a consumer of its event (several features may consume one event); the read loop starts when the app has registered every consumer. */
@@ -62,7 +63,7 @@ export class EventRunnerService implements EventConsumerRegistry, OnApplicationB
62
63
  * each consumer takes a redelivery through its own inbox, so one that already succeeded changes nothing.
63
64
  */
64
65
  async receive(message: InboundMessage): Promise<void> {
65
- const { eventName, envelope, cause: unreadable } = readEnvelopeText(message.value)
66
+ const { eventName, envelope, correlationId, cause: unreadable } = readEnvelopeText(message.value)
66
67
  const consumers = this.consumers.get(eventName) ?? []
67
68
  if (consumers.length === 0) {
68
69
  if (unreadable !== null)
@@ -70,6 +71,27 @@ export class EventRunnerService implements EventConsumerRegistry, OnApplicationB
70
71
  else if (eventName === "") this.logger.warn(EventBusLogEvent.MessageSkipped, { topic: message.topic })
71
72
  return
72
73
  }
74
+ if (correlationId === null) {
75
+ await this.bury(
76
+ message,
77
+ eventName,
78
+ Number(message.headers[ATTEMPT_HEADER] ?? 1),
79
+ "missing or malformed correlation metadata",
80
+ )
81
+ return
82
+ }
83
+ await this.correlation.run(correlationId, () =>
84
+ this.deliver({ ...message, value: JSON.stringify(envelope) }, eventName, envelope, correlationId, consumers),
85
+ )
86
+ }
87
+
88
+ private async deliver(
89
+ message: InboundMessage,
90
+ eventName: string,
91
+ envelope: unknown,
92
+ correlationId: string,
93
+ consumers: ReadonlyArray<EventConsumer<BaseEvent>>,
94
+ ): Promise<void> {
73
95
  const attempt = Number(message.headers[ATTEMPT_HEADER] ?? 1)
74
96
  const notBefore = Number(message.headers[NOT_BEFORE_HEADER] ?? 0)
75
97
  const pause = notBefore - this.clock.now().getTime()
@@ -85,7 +107,7 @@ export class EventRunnerService implements EventConsumerRegistry, OnApplicationB
85
107
  return
86
108
  }
87
109
  try {
88
- await consumer.handle({ eventId: event.eventId, event, attempt })
110
+ await consumer.handle({ eventId: event.eventId, event, attempt, correlationId })
89
111
  } catch (cause) {
90
112
  this.logger.error(EventBusLogEvent.DeliveryFailed, cause, { event: eventName, attempt })
91
113
  failures.push(cause)
@@ -109,7 +131,11 @@ export class EventRunnerService implements EventConsumerRegistry, OnApplicationB
109
131
  topic: retryTopicOf(this.options.topicPrefix, eventName),
110
132
  key: message.key,
111
133
  value: message.value,
112
- headers: { [ATTEMPT_HEADER]: String(attempt + 1), [NOT_BEFORE_HEADER]: String(notBefore) },
134
+ headers: {
135
+ ...message.headers,
136
+ [ATTEMPT_HEADER]: String(attempt + 1),
137
+ [NOT_BEFORE_HEADER]: String(notBefore),
138
+ },
113
139
  },
114
140
  ])
115
141
  this.logger.warn(EventBusLogEvent.DeliveryRetried, { event: eventName, attempt, reason })
@@ -122,6 +148,7 @@ export class EventRunnerService implements EventConsumerRegistry, OnApplicationB
122
148
  key: message.key,
123
149
  value: message.value,
124
150
  headers: {
151
+ ...message.headers,
125
152
  [ATTEMPT_HEADER]: String(attempt),
126
153
  [REASON_HEADER]: reason,
127
154
  [ORIGIN_HEADER]: topicOf(this.options.topicPrefix, eventName),
@@ -3,10 +3,12 @@ import type { MessageBundle } from "@modules/platform/i18n"
3
3
  /** Display text of the event-bus codes, Vietnamese and English. */
4
4
  export const EVENT_BUS_MESSAGES: MessageBundle = {
5
5
  vi: {
6
+ "errors.EVENT_BUS_CORRELATION_INVALID": "M? t??ng quan s? ki?n kh?ng h?p l?.",
6
7
  "errors.EVENT_BUS_BROKER_UNAVAILABLE": "Kênh sự kiện {{operation}} không phản hồi kịp.",
7
8
  "errors.EVENT_BUS_DEAD_LETTER_UNKNOWN": "Không có sự kiện lỗi {{id}} để đưa lại.",
8
9
  },
9
10
  en: {
11
+ "errors.EVENT_BUS_CORRELATION_INVALID": "The event correlation id is invalid.",
10
12
  "errors.EVENT_BUS_BROKER_UNAVAILABLE": "The event broker did not answer {{operation}} in time.",
11
13
  "errors.EVENT_BUS_DEAD_LETTER_UNKNOWN": "There is no dead letter {{id}} to requeue.",
12
14
  },
@@ -11,6 +11,10 @@ export class EventOutboxEntity {
11
11
  @Column({ name: "event_id", type: "varchar", length: 200 })
12
12
  eventId!: string
13
13
 
14
+ /** The originating request id, retained through every relay and delivery. */
15
+ @Column({ name: "correlation_id", type: "varchar", length: 128, default: () => "pg_catalog.gen_random_uuid()::text" })
16
+ correlationId!: string
17
+
14
18
  /** The event name. */
15
19
  @Column({ name: "event_name", type: "varchar", length: 200 })
16
20
  eventName!: string
@@ -23,7 +27,7 @@ export class EventOutboxEntity {
23
27
  @Column({ name: "message_key", type: "varchar", length: 200 })
24
28
  messageKey!: string
25
29
 
26
- /** The envelope `{ eventId, eventName, payload }`. */
30
+ /** The envelope `{ metadata, eventId, eventName, payload }`. */
27
31
  @Column({ name: "envelope", type: "jsonb" })
28
32
  envelope!: object
29
33
 
@@ -1,5 +1,7 @@
1
1
  /** One row the relay reads from the outbox. */
2
2
  export interface OutboxRow {
3
+ /** The originating request id persisted independently of the envelope. */
4
+ readonly correlation_id: string
3
5
  /** The position of the row (a bigint arrives as text). */
4
6
  readonly id: string
5
7
  /** The topic the event travels on. */