@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
@@ -0,0 +1,76 @@
1
+ import { Test } from "@nestjs/testing"
2
+ import { IdempotencyError, IdempotencyErrorCode } from "@modules/platform/idempotency"
3
+ import { requestFingerprint } from "./request-fingerprint.policy"
4
+
5
+ const FINGERPRINT = Symbol("fingerprint")
6
+ const build = async () => {
7
+ const moduleRef = await Test.createTestingModule({
8
+ providers: [{ provide: FINGERPRINT, useValue: requestFingerprint }],
9
+ }).compile()
10
+ return moduleRef.get<typeof requestFingerprint>(FINGERPRINT)
11
+ }
12
+
13
+ describe("requestFingerprint", () => {
14
+ it("hashes nested objects independently of property insertion order", async () => {
15
+ const fingerprint = await build()
16
+
17
+ expect(fingerprint({ personId: "person-1", body: { quantity: 2, productId: "product-1" } })).toBe(
18
+ fingerprint({ body: { productId: "product-1", quantity: 2 }, personId: "person-1" }),
19
+ )
20
+ expect(fingerprint({ a: 1 })).toMatch(/^[a-f0-9]{64}$/)
21
+ })
22
+
23
+ it("keeps binary UTF-16 key order for non-ASCII and supplementary Unicode keys", async () => {
24
+ const fingerprint = await build()
25
+ const body = { "\uE000": 5, "\u{1F600}": 4, "\u00E9": 1, a: 3, Z: 2 }
26
+
27
+ expect(fingerprint(body)).toBe(fingerprint({ Z: 2, a: 3, "\u00E9": 1, "\u{1F600}": 4, "\uE000": 5 }))
28
+ expect(fingerprint(body)).toBe("6eb3469044da5c7df68404ba05b90306384fc773ad605d4de06321a2092ff365")
29
+ })
30
+
31
+ it("preserves array order, JSON types and nested values", async () => {
32
+ const fingerprint = await build()
33
+
34
+ expect(fingerprint([1, 2])).not.toBe(fingerprint([2, 1]))
35
+ expect(fingerprint({ value: 1 })).not.toBe(fingerprint({ value: "1" }))
36
+ expect(fingerprint({ body: { quantity: 2 } })).not.toBe(fingerprint({ body: { quantity: 3 } }))
37
+ expect(new Set([null, true, false, "", 0].map(fingerprint)).size).toBe(5)
38
+ expect(fingerprint([])).not.toBe(fingerprint({}))
39
+ })
40
+
41
+ it("accepts an object without a prototype and repeated noncyclic references", async () => {
42
+ const fingerprint = await build()
43
+ const shared = { quantity: 2 }
44
+ const withoutPrototype: unknown = Object.assign(Object.create(null), { quantity: 2 })
45
+
46
+ expect(fingerprint(withoutPrototype)).toBe(fingerprint(shared))
47
+ expect(fingerprint([shared, shared])).toBe(fingerprint([{ quantity: 2 }, { quantity: 2 }]))
48
+ })
49
+
50
+ it.each([
51
+ [undefined],
52
+ [() => 1],
53
+ [Symbol("unsupported")],
54
+ [BigInt(1)],
55
+ [Number.NaN],
56
+ [Number.POSITIVE_INFINITY],
57
+ [Number.NEGATIVE_INFINITY],
58
+ [{ missing: undefined }],
59
+ [[undefined]],
60
+ [Array(1)],
61
+ [new Date("2026-10-24T00:00:00.000Z")],
62
+ [new Map([["quantity", 2]])],
63
+ ])("refuses values that cannot represent a JSON request: %p", async (request: unknown) => {
64
+ const fingerprint = await build()
65
+
66
+ expect(() => fingerprint(request)).toThrow(new IdempotencyError({ code: IdempotencyErrorCode.RequestInvalid }))
67
+ })
68
+
69
+ it("refuses cyclic requests before a claim can be written", async () => {
70
+ const fingerprint = await build()
71
+ const cyclic: Record<string, unknown> = { quantity: 2 }
72
+ cyclic.body = cyclic
73
+
74
+ expect(() => fingerprint(cyclic)).toThrow(new IdempotencyError({ code: IdempotencyErrorCode.RequestInvalid }))
75
+ })
76
+ })
@@ -0,0 +1,36 @@
1
+ import { createHash } from "node:crypto"
2
+ import { IdempotencyError, IdempotencyErrorCode } from "@modules/platform/idempotency"
3
+
4
+ /** Serializes JSON without depending on object insertion order; array order remains part of the request. */
5
+ const serializeJson = (value: unknown, ancestors: Set<object>): string => {
6
+ if (value === null || typeof value === "string" || typeof value === "boolean") return JSON.stringify(value)
7
+ if (typeof value === "number" && Number.isFinite(value)) return JSON.stringify(value)
8
+ if (typeof value !== "object" || ancestors.has(value))
9
+ throw new IdempotencyError({ code: IdempotencyErrorCode.RequestInvalid })
10
+ ancestors.add(value)
11
+ let serialized: string
12
+ if (Array.isArray(value)) {
13
+ serialized = `[${Array.from(value, (item: unknown) => serializeJson(item, ancestors)).join(",")}]`
14
+ } else {
15
+ const prototype: unknown = Object.getPrototypeOf(value)
16
+ if (prototype !== Object.prototype && prototype !== null) {
17
+ throw new IdempotencyError({ code: IdempotencyErrorCode.RequestInvalid })
18
+ }
19
+ serialized = `{${Object.keys(value)
20
+ .sort((left, right) => Number(left > right) - Number(left < right))
21
+ .map((key) => {
22
+ const item: unknown = Reflect.get(value, key)
23
+ return `${JSON.stringify(key)}:${serializeJson(item, ancestors)}`
24
+ })
25
+ .join(",")}}`
26
+ }
27
+ ancestors.delete(value)
28
+ return serialized
29
+ }
30
+
31
+ /** Returns a stable JSON representation for both the request digest and the stored outcome. */
32
+ export const canonicalJson = (value: unknown): string => serializeJson(value, new Set())
33
+
34
+ /** Hashes the validated client request, so a reused key cannot silently apply a different body. */
35
+ export const requestFingerprint = (request: unknown): string =>
36
+ createHash("sha256").update(canonicalJson(request)).digest("hex")
@@ -4,6 +4,7 @@ import { FencedProcessor } from "./fenced.processor"
4
4
 
5
5
  const claimedJob: ClaimedJob = {
6
6
  jobId: "job-1",
7
+ correlationId: "request-1",
7
8
  kind: "mail",
8
9
  fencingToken: 3,
9
10
  currentStep: null,
@@ -32,7 +32,7 @@ const buildWithoutConnection = async () => {
32
32
  return { claims: moduleRef.get(JobClaimService) }
33
33
  }
34
34
 
35
- const claimed: ClaimedJob = { jobId: "j-1", kind: "mail", fencingToken: 3, currentStep: null, payload: { id: "m-1" } }
35
+ const claimed: ClaimedJob = { correlationId: "request-1", jobId: "j-1", kind: "mail", fencingToken: 3, currentStep: null, payload: { id: "m-1" } }
36
36
 
37
37
  describe("JobClaimService", () => {
38
38
  describe("claim", () => {
@@ -48,6 +48,7 @@ describe("JobClaimService", () => {
48
48
  const job = await claims.claim({
49
49
  kind: "mail",
50
50
  jobKey: "k-1",
51
+ correlationId: "request-1",
51
52
  payload: { id: "m-1" },
52
53
  workerId: "w-1",
53
54
  leaseMs: 60000,
@@ -69,7 +70,7 @@ describe("JobClaimService", () => {
69
70
  const { claims } = await build(manager)
70
71
 
71
72
  await expect(
72
- claims.claim({ kind: "mail", jobKey: "k-1", payload: {}, workerId: "w-1", leaseMs: 1000 }),
73
+ claims.claim({ correlationId: "request-1", kind: "mail", jobKey: "k-1", payload: {}, workerId: "w-1", leaseMs: 1000 }),
73
74
  ).resolves.toBeNull()
74
75
  })
75
76
  })
@@ -141,7 +142,7 @@ describe("JobClaimService", () => {
141
142
  const { claims } = await buildWithoutConnection()
142
143
 
143
144
  await expect(
144
- claims.claim({ kind: "mail", jobKey: "k-1", payload: {}, workerId: "w-1", leaseMs: 1000 }),
145
+ claims.claim({ correlationId: "request-1", kind: "mail", jobKey: "k-1", payload: {}, workerId: "w-1", leaseMs: 1000 }),
145
146
  ).rejects.toMatchObject({
146
147
  code: JobsErrorCode.ConnectionMissing,
147
148
  })
@@ -44,8 +44,10 @@ export class JobClaimService implements JobClaims {
44
44
  ])
45
45
  const row = rows[0]
46
46
  if (row === undefined) return null
47
+ // The durable delivery supplies the same request id on every retry; the job fence identifies a claim separately.
47
48
  return {
48
49
  jobId: row.id,
50
+ correlationId: params.correlationId,
49
51
  kind: row.kind,
50
52
  fencingToken: Number(row.fencing_token),
51
53
  currentStep: row.current_step,
@@ -14,8 +14,8 @@ import type { JobClaims } from "./jobs.port"
14
14
  import { JobRunnerService } from "./job-runner.service"
15
15
 
16
16
  const options: JobsOptions = { connection: "primary", workerId: "w-1", leaseMs: 60000 }
17
- const delivery: QueueDelivery = { id: "k-1", queue: "mail", payload: { id: "m-1" }, attempt: 1 }
18
- const job: ClaimedJob = { jobId: "j-1", kind: "mail", fencingToken: 2, currentStep: null, payload: { id: "m-1" } }
17
+ const delivery: QueueDelivery = { correlationId: "request-1", id: "k-1", queue: "mail", payload: { id: "m-1" }, attempt: 1 }
18
+ const job: ClaimedJob = { correlationId: "request-1", jobId: "j-1", kind: "mail", fencingToken: 2, currentStep: null, payload: { id: "m-1" } }
19
19
  const fenced = (): JobsError => new JobsError({ code: JobsErrorCode.FencedOut, params: { jobId: "j-1", token: 2 } })
20
20
 
21
21
  const build = async () => {
@@ -66,6 +66,7 @@ describe("JobRunnerService", () => {
66
66
  expect(claims.claim).toHaveBeenCalledWith({
67
67
  kind: "mail",
68
68
  jobKey: "k-1",
69
+ correlationId: "request-1",
69
70
  payload: { id: "m-1" },
70
71
  workerId: "w-1",
71
72
  leaseMs: 60000,
@@ -75,6 +76,23 @@ describe("JobRunnerService", () => {
75
76
  expect(claims.fail).not.toHaveBeenCalled()
76
77
  })
77
78
 
79
+ it("retains the original request correlation when a retry claims a newer fence", async () => {
80
+ const { runner, claims, processor } = await build()
81
+ const failure = new Error("provider unavailable")
82
+ claims.claim.mockResolvedValueOnce(job).mockResolvedValueOnce({ ...job, fencingToken: 3 })
83
+ processor.process.mockRejectedValueOnce(failure).mockResolvedValueOnce(undefined)
84
+
85
+ await expect(runner.run(processor, delivery)).rejects.toBe(failure)
86
+ await runner.run(processor, { ...delivery, attempt: 2 })
87
+
88
+ expect(processor.process.mock.calls.map(([claimed]) => claimed.correlationId)).toEqual([
89
+ "request-1",
90
+ "request-1",
91
+ ])
92
+ expect(processor.process.mock.calls.map(([claimed]) => claimed.fencingToken)).toEqual([2, 3])
93
+ expect(claims.complete).toHaveBeenCalledWith({ jobId: "j-1", expectedFencingToken: 3 })
94
+ })
95
+
78
96
  it("does nothing when the job is done or another worker holds a live claim", async () => {
79
97
  const { runner, claims, processor } = await build()
80
98
  claims.claim.mockResolvedValue(null)
@@ -38,6 +38,7 @@ export class JobRunnerService implements JobProcessorRegistry {
38
38
  const job = await this.claims.claim({
39
39
  kind: processor.queue,
40
40
  jobKey: delivery.id,
41
+ correlationId: delivery.correlationId,
41
42
  payload: delivery.payload,
42
43
  workerId: this.options.workerId,
43
44
  leaseMs: this.options.leaseMs,
@@ -1,5 +1,7 @@
1
1
  /** A job a worker claimed: the token it must pass to every write. */
2
2
  export interface ClaimedJob {
3
+ /** The originating request id carried explicitly by the durable queue delivery. */
4
+ readonly correlationId: string
3
5
  /** The id of the job row. */
4
6
  readonly jobId: string
5
7
  /** The kind of the job: the queue its processor consumes. */
@@ -14,6 +16,8 @@ export interface ClaimedJob {
14
16
 
15
17
  /** What a worker asks for when it claims a delivery. */
16
18
  export interface ClaimParams {
19
+ /** The originating request id retained across delivery retries. */
20
+ readonly correlationId: string
17
21
  /** The kind of the job. */
18
22
  readonly kind: string
19
23
  /** The key of the delivery: the BullMQ job id. A second claim of the same key bumps the token of the same row. */
@@ -0,0 +1,10 @@
1
+ export { JOBS_ERROR_KINDS } from "./errors/jobs.error"
2
+ export { parseJobsConfig } from "./jobs.config"
3
+ export type { JobsConfig } from "./jobs.config"
4
+ export { isRunKey } from "./jobs.contracts"
5
+ export type { ClaimedJob, RunKey } from "./jobs.contracts"
6
+ export { InjectJobClaims, InjectJobProcessorRegistry } from "./jobs.decorators"
7
+ export { JobsModule } from "./jobs.module"
8
+ export type { JobClaims, JobProcessorRegistry, JobStep } from "./jobs.port"
9
+ export { FencedProcessor } from "./fenced.processor"
10
+ export { JOBS_MESSAGES } from "./messages/jobs.messages"
@@ -0,0 +1,44 @@
1
+ -- The fenced jobs: one row per delivery key. A claim bumps the fencing token and every
2
+ -- other write of a worker carries the token it holds, so a worker that lost its claim
3
+ -- changes nothing. Only the app_be connection reaches it.
4
+ create table public.jobs (
5
+ id uuid primary key default gen_random_uuid(),
6
+ kind varchar(200) not null,
7
+ job_key varchar(200) not null,
8
+ status varchar(16) not null,
9
+ current_step varchar(200),
10
+ fencing_token bigint not null,
11
+ claimed_by varchar(200),
12
+ lease_expires_at timestamptz,
13
+ payload jsonb not null,
14
+ error text,
15
+ created_at timestamptz not null,
16
+ updated_at timestamptz not null,
17
+ constraint jobs_job_key_key unique (job_key),
18
+ constraint jobs_status_check check (status in ('running', 'failed', 'done')),
19
+ constraint jobs_fencing_token_check check (fencing_token >= 1)
20
+ );
21
+
22
+ alter table public.jobs enable row level security;
23
+ alter table public.jobs force row level security;
24
+
25
+ create trigger jobs_set_updated_at
26
+ before update on public.jobs
27
+ for each row
28
+ execute function private.set_updated_at();
29
+
30
+ create policy jobs_app_be_select
31
+ on public.jobs for select to app_be
32
+ using (kind is not null);
33
+
34
+ create policy jobs_app_be_insert
35
+ on public.jobs for insert to app_be
36
+ with check (kind is not null);
37
+
38
+ create policy jobs_app_be_update
39
+ on public.jobs for update to app_be
40
+ using (kind is not null)
41
+ with check (kind is not null);
42
+
43
+ revoke all on table public.jobs from public, anon, authenticated;
44
+ grant select, insert, update on table public.jobs to app_be;
@@ -0,0 +1,33 @@
1
+ import { sql } from "@modules/platform/database"
2
+
3
+ /**
4
+ * Claims a delivery in one statement ($1 kind, $2 job key, $3 worker, $4 lease end, $5 payload, $6 instant). A new key inserts the row
5
+ * with token 1; an existing key is claimed again only when the job failed or its lease expired, and then the token is bumped by the
6
+ * store. A done job and a live claim match nothing, so no row comes back.
7
+ */
8
+ export const CLAIM_JOB = sql`INSERT INTO public.jobs (kind, job_key, status, fencing_token, claimed_by, lease_expires_at, payload, created_at, updated_at)
9
+ VALUES ($1, $2, 'running', 1, $3, $4, $5::jsonb, $6, $6)
10
+ ON CONFLICT (job_key) DO UPDATE
11
+ SET fencing_token = jobs.fencing_token + 1, claimed_by = $3, lease_expires_at = $4, status = 'running', error = NULL, updated_at = $6
12
+ WHERE jobs.status = 'failed' OR (jobs.status = 'running' AND jobs.lease_expires_at < $6)
13
+ RETURNING id, kind, fencing_token, current_step, payload`
14
+
15
+ /** The guarded writes wrap their UPDATE in a CTE: TypeORM returns a bare `UPDATE ... RETURNING` as `[rows, count]`, a SELECT as rows. */
16
+
17
+ /** Records a step ($1 job id, $2 expected token, $3 step, $4 instant); no row comes back when the token is stale. */
18
+ export const ADVANCE_JOB = sql`WITH changed AS (UPDATE public.jobs SET current_step = $3, updated_at = $4
19
+ WHERE id = $1 AND fencing_token = $2 AND status = 'running'
20
+ RETURNING id)
21
+ SELECT id FROM changed`
22
+
23
+ /** Completes the job ($1 job id, $2 expected token, $3 instant). */
24
+ export const COMPLETE_JOB = sql`WITH changed AS (UPDATE public.jobs SET status = 'done', lease_expires_at = NULL, updated_at = $3
25
+ WHERE id = $1 AND fencing_token = $2 AND status = 'running'
26
+ RETURNING id)
27
+ SELECT id FROM changed`
28
+
29
+ /** Fails the job ($1 job id, $2 expected token, $3 reason, $4 instant). */
30
+ export const FAIL_JOB = sql`WITH changed AS (UPDATE public.jobs SET status = 'failed', error = $3, lease_expires_at = NULL, updated_at = $4
31
+ WHERE id = $1 AND fencing_token = $2 AND status = 'running'
32
+ RETURNING id)
33
+ SELECT id FROM changed`
@@ -1,14 +1,19 @@
1
1
  import { Test } from "@nestjs/testing"
2
- import { mock } from "@starci/jest-preset"
2
+ import { fakeIds, mock } from "@starci/jest-preset"
3
+ import { LOGGER } from "@modules/platform/logging"
4
+ import type { Logger } from "@modules/platform/logging"
5
+ import { IDS } from "@modules/platform/ids"
6
+ import { QueueErrorCode } from "./errors/queue.error"
3
7
  import { BullmqQueueTransportClient } from "./bullmq-queue-transport.client"
8
+ import { BULLMQ_JOB_SCHEMA } from "./queue.contracts"
4
9
  import type { QueueHandler } from "./queue.contracts"
5
10
  import { QUEUE_FACTORY, QUEUE_OPTIONS } from "./queue.decorators"
11
+ import { QueueLogEvent } from "./queue.log-events"
6
12
  import type { QueueOptions } from "./queue.options"
7
13
  import { ATTEMPTS, BACKOFF_MS, KEEP_COMPLETED_SECONDS } from "./queue.policy"
8
14
  import type { BullmqJob, BullmqQueue, BullmqWorker, QueueFactory } from "./queue-transport.port"
9
15
 
10
- /** Asymmetric matchers read as `unknown`: jest types them `any`, which may not flow into a property or a typed argument. */
11
- const anyOf = (type: unknown): unknown => expect.any(type)
16
+ const MINTED_ID = "00000000-0000-4000-8000-000000000001"
12
17
 
13
18
  const options: QueueOptions = {
14
19
  redisHost: "redis.test",
@@ -26,6 +31,8 @@ const build = async () => {
26
31
  const queue = mock<BullmqQueue>()
27
32
  const worker = mock<BullmqWorker>()
28
33
  const factory = mock<QueueFactory>()
34
+ const ids = fakeIds()
35
+ const logger = mock<Logger>()
29
36
  factory.queue.mockReturnValue(queue)
30
37
  factory.worker.mockReturnValue(worker)
31
38
  const moduleRef = await Test.createTestingModule({
@@ -33,30 +40,46 @@ const build = async () => {
33
40
  BullmqQueueTransportClient,
34
41
  { provide: QUEUE_OPTIONS, useValue: options },
35
42
  { provide: QUEUE_FACTORY, useValue: factory },
43
+ { provide: IDS, useValue: ids },
44
+ { provide: LOGGER, useValue: logger },
36
45
  ],
37
46
  }).compile()
38
- return { client: moduleRef.get(BullmqQueueTransportClient), queue, worker, factory }
47
+ return { client: moduleRef.get(BullmqQueueTransportClient), queue, worker, factory, logger, ids }
39
48
  }
40
49
 
41
- describe("BullmqQueueTransportClient", () => {
50
+ describe("BullmqQueueTransportClient jobs and schedulers", () => {
42
51
  it("adds idempotent jobs with the queue retry and retention policy", async () => {
43
52
  const { client, queue, factory } = await build()
44
53
  const payload = { orderId: "order-1" }
45
54
 
46
- await client.add("orders", "outbox-1", payload)
47
- await client.add("orders", "outbox-2", payload)
55
+ await client.add("orders", "outbox-1", payload, "request-1")
56
+ await client.add("orders", "outbox-2", payload, "request-2")
48
57
 
49
58
  expect(factory.queue).toHaveBeenCalledTimes(1)
50
59
  expect(factory.queue).toHaveBeenCalledWith("orders", {
51
60
  prefix: "spec.",
52
61
  connection: { host: "redis.test", port: 6380, db: 7, maxRetriesPerRequest: null },
53
62
  })
54
- expect(queue.add).toHaveBeenNthCalledWith(1, "orders", payload, {
55
- jobId: "outbox-1",
56
- attempts: ATTEMPTS,
57
- backoff: { type: "exponential", delay: BACKOFF_MS },
58
- removeOnComplete: { age: KEEP_COMPLETED_SECONDS },
63
+ expect(queue.add).toHaveBeenNthCalledWith(
64
+ 1,
65
+ "orders",
66
+ { schema: BULLMQ_JOB_SCHEMA, kind: "enqueued", payload, correlationId: "request-1" },
67
+ {
68
+ jobId: "outbox-1",
69
+ attempts: ATTEMPTS,
70
+ backoff: { type: "exponential", delay: BACKOFF_MS },
71
+ removeOnComplete: { age: KEEP_COMPLETED_SECONDS },
72
+ },
73
+ )
74
+ })
75
+
76
+ it("rejects unsafe correlation before writing a new job", async () => {
77
+ const { client, factory } = await build()
78
+
79
+ await expect(client.add("orders", "outbox-1", {}, "unsafe\nrequest")).rejects.toMatchObject({
80
+ code: QueueErrorCode.CorrelationInvalid,
59
81
  })
82
+ expect(factory.queue).not.toHaveBeenCalled()
60
83
  })
61
84
 
62
85
  it("upserts schedulers with their payload or an empty payload", async () => {
@@ -69,16 +92,26 @@ describe("BullmqQueueTransportClient", () => {
69
92
  1,
70
93
  "settlement",
71
94
  { every: 60_000 },
72
- { name: "billing", data: { shard: 2 }, opts: { attempts: ATTEMPTS } },
95
+ {
96
+ name: "billing",
97
+ data: { schema: BULLMQ_JOB_SCHEMA, kind: "scheduled", payload: { shard: 2 } },
98
+ opts: { attempts: ATTEMPTS },
99
+ },
73
100
  )
74
101
  expect(queue.upsertJobScheduler).toHaveBeenNthCalledWith(
75
102
  2,
76
103
  "cleanup",
77
104
  { every: 120_000 },
78
- { name: "billing", data: {}, opts: { attempts: ATTEMPTS } },
105
+ {
106
+ name: "billing",
107
+ data: { schema: BULLMQ_JOB_SCHEMA, kind: "scheduled", payload: {} },
108
+ opts: { attempts: ATTEMPTS },
109
+ },
79
110
  )
80
111
  })
112
+ })
81
113
 
114
+ describe("BullmqQueueTransportClient worker deliveries", () => {
82
115
  it("starts a worker that translates BullMQ jobs into queue deliveries", async () => {
83
116
  const { client, factory } = await build()
84
117
  const handler = mock<QueueHandler>({ queue: "orders" })
@@ -86,29 +119,181 @@ describe("BullmqQueueTransportClient", () => {
86
119
 
87
120
  await client.work(handler, 3)
88
121
 
89
- expect(factory.worker).toHaveBeenCalledWith("orders", anyOf(Function), {
122
+ expect(factory.worker).toHaveBeenCalledWith("orders", expect.any(Function), {
90
123
  prefix: "spec.",
91
124
  connection: { host: "redis.test", port: 6380, db: 7, maxRetriesPerRequest: null },
92
125
  concurrency: 3,
93
126
  })
94
127
  const processor = factory.worker.mock.calls[0]?.[1]
95
- await processor?.(mock<BullmqJob>({ id: "job-1", data: { orderId: "order-1" }, attemptsMade: 2 }))
96
- await processor?.({ data: { orderId: "order-2" }, attemptsMade: 0 } satisfies BullmqJob)
128
+ await processor?.(
129
+ mock<BullmqJob>({
130
+ id: "job-1",
131
+ data: {
132
+ schema: BULLMQ_JOB_SCHEMA,
133
+ kind: "enqueued",
134
+ payload: { orderId: "order-1" },
135
+ correlationId: "request-1",
136
+ },
137
+ attemptsMade: 2,
138
+ }),
139
+ )
140
+ await processor?.({
141
+ data: { schema: BULLMQ_JOB_SCHEMA, kind: "scheduled", payload: { orderId: "order-2" } },
142
+ attemptsMade: 0,
143
+ } satisfies BullmqJob)
144
+ await processor?.({
145
+ id: "tick-3",
146
+ data: { schema: BULLMQ_JOB_SCHEMA, kind: "scheduled", payload: {} },
147
+ attemptsMade: 1,
148
+ } satisfies BullmqJob)
97
149
 
98
150
  expect(handler.handle).toHaveBeenNthCalledWith(1, {
99
151
  id: "job-1",
152
+ correlationId: "request-1",
100
153
  queue: "orders",
101
154
  payload: { orderId: "order-1" },
102
155
  attempt: 3,
103
156
  })
104
157
  expect(handler.handle).toHaveBeenNthCalledWith(2, {
105
158
  id: "",
159
+ correlationId: MINTED_ID,
106
160
  queue: "orders",
107
161
  payload: { orderId: "order-2" },
108
162
  attempt: 1,
109
163
  })
164
+ expect(handler.handle).toHaveBeenNthCalledWith(3, {
165
+ id: "tick-3",
166
+ correlationId: "tick-3",
167
+ queue: "orders",
168
+ payload: {},
169
+ attempt: 2,
170
+ })
171
+ })
172
+ })
173
+
174
+ describe("BullmqQueueTransportClient stored job upgrades", () => {
175
+ it("preserves raw payloads and the same job correlation through legacy retries", async () => {
176
+ const { client, factory, ids } = await build()
177
+ const handler = mock<QueueHandler>({ queue: "orders" })
178
+ await client.work(handler, 3)
179
+ const processor = factory.worker.mock.calls[0]?.[1]
180
+ const payload = { orderId: "before-upgrade", kind: "expire-order", payload: { reason: "unpaid" } }
181
+
182
+ await processor?.({ id: "legacy-job", data: payload, attemptsMade: 0 })
183
+ await processor?.({ id: "legacy-job", data: payload, attemptsMade: 1 })
184
+
185
+ expect(handler.handle).toHaveBeenNthCalledWith(1, {
186
+ id: "legacy-job",
187
+ queue: "orders",
188
+ payload,
189
+ correlationId: "legacy-job",
190
+ attempt: 1,
191
+ })
192
+ expect(handler.handle).toHaveBeenNthCalledWith(2, {
193
+ id: "legacy-job",
194
+ queue: "orders",
195
+ payload,
196
+ correlationId: "legacy-job",
197
+ attempt: 2,
198
+ })
199
+ expect(ids.issued).toEqual([])
200
+ })
201
+
202
+ it.each(["enqueued", "scheduled"])("keeps legacy business fields even when kind is %s", async (kind) => {
203
+ const { client, factory, ids } = await build()
204
+ const handler = mock<QueueHandler>({ queue: "orders" })
205
+ await client.work(handler, 3)
206
+ const processor = factory.worker.mock.calls[0]?.[1]
207
+ const payload = {
208
+ schema: "order/business@1",
209
+ kind,
210
+ correlationId: "business\nkey",
211
+ payload: { orderId: "legacy-order" },
212
+ }
213
+
214
+ await processor?.({ id: "legacy-collision", data: payload, attemptsMade: 0 })
215
+
216
+ expect(handler.handle).toHaveBeenCalledWith({
217
+ id: "legacy-collision",
218
+ queue: "orders",
219
+ payload,
220
+ correlationId: "legacy-collision",
221
+ attempt: 1,
222
+ })
223
+ expect(ids.issued).toEqual([])
224
+ })
225
+ })
226
+
227
+ describe("BullmqQueueTransportClient correlation roots and marked envelopes", () => {
228
+ it("mints only when legacy data has no job id and accepts explicit scheduled correlation", async () => {
229
+ const { client, factory, ids } = await build()
230
+ const handler = mock<QueueHandler>({ queue: "orders" })
231
+ await client.work(handler, 3)
232
+ const processor = factory.worker.mock.calls[0]?.[1]
233
+
234
+ await processor?.({ data: { orderId: "legacy-no-id" }, attemptsMade: 0 })
235
+ await processor?.({
236
+ data: { schema: BULLMQ_JOB_SCHEMA, kind: "scheduled", payload: {}, correlationId: "upstream-tick" },
237
+ attemptsMade: 0,
238
+ })
239
+
240
+ expect(handler.handle.mock.calls.map(([delivery]) => delivery.correlationId)).toEqual([
241
+ MINTED_ID,
242
+ "upstream-tick",
243
+ ])
244
+ expect(ids.issued).toEqual([MINTED_ID])
245
+ })
246
+
247
+ it.each<unknown>([
248
+ null,
249
+ { schema: "starci/queue@2", kind: "enqueued", payload: {}, correlationId: "request-1" },
250
+ { schema: BULLMQ_JOB_SCHEMA, kind: "unknown", payload: {} },
251
+ { schema: BULLMQ_JOB_SCHEMA, kind: "enqueued", payload: {}, correlationId: undefined },
252
+ { schema: BULLMQ_JOB_SCHEMA, kind: "enqueued", payload: null, correlationId: "request-1" },
253
+ { schema: BULLMQ_JOB_SCHEMA, kind: "enqueued", payload: {}, correlationId: "bad\nidentity" },
254
+ { schema: BULLMQ_JOB_SCHEMA, kind: "scheduled", payload: {}, correlationId: undefined },
255
+ ])("rejects malformed current data before business delivery (%j)", async (data) => {
256
+ const { client, factory } = await build()
257
+ const handler = mock<QueueHandler>({ queue: "orders" })
258
+ await client.work(handler, 3)
259
+ const processor = factory.worker.mock.calls[0]?.[1]
260
+
261
+ await expect(processor?.({ id: "stored-job", data, attemptsMade: 0 })).rejects.toMatchObject({
262
+ code: QueueErrorCode.MalformedMessage,
263
+ })
264
+ expect(handler.handle).not.toHaveBeenCalled()
265
+ })
266
+ })
267
+
268
+ describe("BullmqQueueTransportClient driver errors", () => {
269
+ it("routes queue driver errors through the logger once per queue", async () => {
270
+ const { client, queue, logger } = await build()
271
+ await client.add("orders", "outbox-1", {}, "request-1")
272
+ await client.add("orders", "outbox-2", {}, "request-2")
273
+ const cause = new Error("redis connection lost")
274
+
275
+ const listener = queue.on.mock.calls[0]?.[1]
276
+ listener?.(cause)
277
+
278
+ expect(queue.on).toHaveBeenCalledTimes(1)
279
+ expect(queue.on).toHaveBeenCalledWith("error", expect.any(Function))
280
+ expect(logger.error).toHaveBeenCalledWith(QueueLogEvent.QueueFailed, cause, { queue: "orders" })
281
+ })
282
+
283
+ it("routes worker driver errors through the logger", async () => {
284
+ const { client, worker, logger } = await build()
285
+ await client.work(mock<QueueHandler>({ queue: "orders" }), 3)
286
+ const cause = new Error("blocking redis connection lost")
287
+
288
+ const listener = worker.on.mock.calls[0]?.[1]
289
+ listener?.(cause)
290
+
291
+ expect(worker.on).toHaveBeenCalledWith("error", expect.any(Function))
292
+ expect(logger.error).toHaveBeenCalledWith(QueueLogEvent.WorkerFailed, cause, { queue: "orders" })
110
293
  })
294
+ })
111
295
 
296
+ describe("BullmqQueueTransportClient wait and shutdown", () => {
112
297
  it("waits for the requested delay", async () => {
113
298
  jest.useFakeTimers()
114
299
  try {
@@ -132,7 +317,7 @@ describe("BullmqQueueTransportClient", () => {
132
317
  finishWorkerClose = resolve
133
318
  }),
134
319
  )
135
- await client.add("orders", "outbox-1", {})
320
+ await client.add("orders", "outbox-1", {}, "request-1")
136
321
  await client.work(handler, 3)
137
322
 
138
323
  const closing = client.close()