@starci/hfs 4.7.31 → 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 +12 -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,40 @@
1
+ /** The HTTP methods the client sends. */
2
+ export type HttpMethod = "GET" | "POST" | "PUT" | "PATCH" | "DELETE"
3
+
4
+ /** One outbound request. Callers supply the deadline and correlation id explicitly. */
5
+ export interface HttpRequest {
6
+ /** The correlation id carried explicitly from the inbound request or delivery; sent as `x-request-id`. */
7
+ readonly correlationId: string
8
+ /** The HTTP method. */
9
+ readonly method: HttpMethod
10
+ /** The absolute URL. */
11
+ readonly url: string
12
+ /** Extra request headers; correlation headers come from `correlationId` instead. */
13
+ readonly headers?: Readonly<Record<string, string>>
14
+ /** A JSON-serializable body, sent as `application/json`. */
15
+ readonly body?: unknown
16
+ /** Form fields, sent as `application/x-www-form-urlencoded`; use either `body` or `form`, not both. */
17
+ readonly form?: Readonly<Record<string, string>>
18
+ /** Raw bytes sent as they are (the caller names their `content-type`); use instead of `body` and `form`. */
19
+ readonly bytes?: Buffer
20
+ /** How the answer body is read: parsed as JSON (default) or kept as raw bytes (a Buffer, empty when the answer had none). */
21
+ readonly read?: "json" | "bytes"
22
+ /** How long to wait for the answer before the call fails as a timeout. */
23
+ readonly timeoutMs: number
24
+ /** Cancels the call when aborted. */
25
+ readonly signal?: AbortSignal
26
+ }
27
+
28
+ /** The answer to one request: the status and the body parsed as JSON (undefined when the body is empty), or its raw bytes. */
29
+ export interface HttpResponse {
30
+ /** The HTTP status code. */
31
+ readonly status: number
32
+ /** The parsed JSON body (undefined when the answer had none), or a Buffer when the request asked for `read: "bytes"`. */
33
+ readonly body: unknown
34
+ }
35
+
36
+ /** The outbound HTTP port: the only way product code calls another service. */
37
+ export interface HttpClient {
38
+ /** Sends `request`; rejects with an HttpError on a network failure, a timeout or an unreadable body. */
39
+ request(request: HttpRequest): Promise<HttpResponse>
40
+ }
@@ -0,0 +1,7 @@
1
+ export { HTTP_ERROR_KINDS, HttpError, HttpErrorCode } from "./errors/http.error"
2
+ export { HTTP_MESSAGES } from "./messages/http.messages"
3
+ export { callGraphql } from "./graphql-call.mapper"
4
+ export type { GraphqlAnswer } from "./graphql-call.mapper"
5
+ export { HTTP_CLIENT, InjectHttpClient } from "./http.decorators"
6
+ export { HttpModule } from "./http.module"
7
+ export type { HttpClient } from "./http.port"
@@ -0,0 +1,15 @@
1
+ import type { MessageBundle } from "@modules/platform/i18n"
2
+
3
+ /** Display text of the http capability codes, Vietnamese and English. */
4
+ export const HTTP_MESSAGES: MessageBundle = {
5
+ vi: {
6
+ "errors.HTTP_TIMEOUT": "Dịch vụ phụ thuộc không phản hồi kịp thời.",
7
+ "errors.HTTP_NETWORK": "Không thể kết nối tới dịch vụ phụ thuộc.",
8
+ "errors.HTTP_BODY_UNREADABLE": "Dịch vụ phụ thuộc trả về dữ liệu không đọc được.",
9
+ },
10
+ en: {
11
+ "errors.HTTP_TIMEOUT": "A dependent service did not answer in time.",
12
+ "errors.HTTP_NETWORK": "A dependent service could not be reached.",
13
+ "errors.HTTP_BODY_UNREADABLE": "A dependent service answered with an unreadable body.",
14
+ },
15
+ }
@@ -0,0 +1,9 @@
1
+ import { injector } from "@modules/platform/composition"
2
+ import type { TypedParameterDecorator } from "@modules/platform/composition"
3
+ import type { Ids } from "./ids.port"
4
+
5
+ /** Token of the Ids port. */
6
+ export const IDS: unique symbol = Symbol("platform.ids")
7
+
8
+ /** Injects the Ids port. Parameter type: Ids. */
9
+ export const InjectIds = (): TypedParameterDecorator<Ids> => injector<Ids>(IDS)
@@ -0,0 +1,7 @@
1
+ import { ConfigurableModuleBuilder } from "@nestjs/common"
2
+ import type { IdsOptions } from "./ids.options"
3
+
4
+ /** The configurable-module base of the ids capability; `isGlobal` is decided by the app root. */
5
+ export const { ConfigurableModuleClass, OPTIONS_TYPE } = new ConfigurableModuleBuilder<IdsOptions>()
6
+ .setExtras({ isGlobal: false }, (definition, extras) => ({ ...definition, global: extras.isGlobal }))
7
+ .build()
@@ -0,0 +1,19 @@
1
+ import { Module } from "@nestjs/common"
2
+ import type { DynamicModule } from "@nestjs/common"
3
+ import { IDS } from "./ids.decorators"
4
+ import { ConfigurableModuleClass, OPTIONS_TYPE } from "./ids.module-definition"
5
+ import { SystemIds } from "./system-ids.service"
6
+
7
+ @Module({})
8
+ /** Provides the Ids port backed by the system random source. */
9
+ export class IdsModule extends ConfigurableModuleClass {
10
+ /** Registers the capability once per app. */
11
+ static register(options: typeof OPTIONS_TYPE): DynamicModule {
12
+ const base = super.register(options)
13
+ return {
14
+ ...base,
15
+ providers: [...(base.providers ?? []), { provide: IDS, useClass: SystemIds }],
16
+ exports: [IDS],
17
+ }
18
+ }
19
+ }
@@ -0,0 +1,2 @@
1
+ /** The ids capability takes no options; the empty record keeps `isGlobal` assignable through the configurable module. */
2
+ export type IdsOptions = Record<never, never>
@@ -0,0 +1,5 @@
1
+ /** The ids port: business code asks it for a new identifier instead of reading a random source, so a spec can say every id. */
2
+ export interface Ids {
3
+ /** A new unique identifier (a UUID). */
4
+ next(): string
5
+ }
@@ -0,0 +1,4 @@
1
+ export type { Ids } from "./ids.port"
2
+ export { IDS, InjectIds } from "./ids.decorators"
3
+ export { IdsModule } from "./ids.module"
4
+ export { SystemIds } from "./system-ids.service"
@@ -0,0 +1,15 @@
1
+ import { Test } from "@nestjs/testing"
2
+ import { SystemIds } from "./system-ids.service"
3
+
4
+ describe("SystemIds", () => {
5
+ it("answers a UUID, and a different one each time", async () => {
6
+ const moduleRef = await Test.createTestingModule({ providers: [SystemIds] }).compile()
7
+ const ids = moduleRef.get(SystemIds)
8
+
9
+ const first = ids.next()
10
+ const second = ids.next()
11
+
12
+ expect(first).toMatch(/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/)
13
+ expect(second).not.toBe(first)
14
+ })
15
+ })
@@ -0,0 +1,10 @@
1
+ import { randomUUID } from "node:crypto"
2
+ import type { Ids } from "./ids.port"
3
+
4
+ /** The production ids: the one place that reads the ambient random source. It has no dependencies, so the DI container builds it without a decorator. */
5
+ export class SystemIds implements Ids {
6
+ /** A new random UUID. */
7
+ next(): string {
8
+ return randomUUID()
9
+ }
10
+ }
@@ -0,0 +1,59 @@
1
+ import type { INestApplication } from "@nestjs/common"
2
+ import { Test } from "@nestjs/testing"
3
+ import { mock } from "@starci/jest-preset"
4
+ import type { NextFunction, Request, RequestHandler, Response } from "express"
5
+ import type { CorrelationIds, CorrelationRunner } from "./correlation.port"
6
+ import { applyCorrelation, correlationMiddleware } from "./correlation-request.policy"
7
+ import { CORRELATION_IDS, CORRELATION_RUNNER } from "./logging.decorators"
8
+ import { runWithCorrelation, stampLogFields } from "./correlation.policy"
9
+
10
+ const SUBJECT = Symbol("request-correlation-policy")
11
+ const build = async () => {
12
+ const moduleRef = await Test.createTestingModule({
13
+ providers: [{ provide: SUBJECT, useValue: { applyCorrelation, correlationMiddleware } }],
14
+ }).compile()
15
+ return moduleRef.get<typeof import("./correlation-request.policy")>(SUBJECT)
16
+ }
17
+
18
+ describe("request correlation", () => {
19
+ it("copies the explicit identity to the request and response and scopes downstream work", async () => {
20
+ const subject = await build()
21
+ const ids = mock<CorrelationIds>({ fromHeaders: () => "request-id" })
22
+ const runner = mock<CorrelationRunner>({ run: runWithCorrelation })
23
+ const request = mock<Request>({ headers: { "x-request-id": "request-id" } })
24
+ const setHeader = jest.fn()
25
+ const response = mock<Response>({ setHeader })
26
+ const next: NextFunction = jest.fn(() => {
27
+ expect(stampLogFields({}, "root-id")).toEqual({ correlationId: "request-id" })
28
+ })
29
+
30
+ subject.correlationMiddleware(ids, runner)(request, response, next)
31
+
32
+ expect(request).toHaveProperty("correlationId", "request-id")
33
+ expect(setHeader).toHaveBeenCalledWith("x-request-id", "request-id")
34
+ expect(next).toHaveBeenCalledTimes(1)
35
+ })
36
+
37
+ it("installs the middleware using the registered logging ports", async () => {
38
+ const subject = await build()
39
+ const ids = mock<CorrelationIds>({ fromHeaders: () => "installed-id" })
40
+ const runner = mock<CorrelationRunner>({ run: runWithCorrelation })
41
+ const installed: Array<RequestHandler> = []
42
+ const app: INestApplication = mock<INestApplication>({
43
+ get: jest.fn().mockImplementation((key: unknown) => (key === CORRELATION_IDS ? ids : runner)),
44
+ use: (handler: RequestHandler): INestApplication => {
45
+ installed.push(handler)
46
+ return app
47
+ },
48
+ })
49
+ subject.applyCorrelation(app)
50
+
51
+ expect(app.get).toHaveBeenCalledWith(CORRELATION_IDS)
52
+ expect(app.get).toHaveBeenCalledWith(CORRELATION_RUNNER)
53
+
54
+ expect(installed).toHaveLength(1)
55
+ const setHeader = jest.fn()
56
+ installed[0]?.(mock<Request>({ headers: {} }), mock<Response>({ setHeader }), jest.fn())
57
+ expect(setHeader).toHaveBeenCalledWith("x-request-id", "installed-id")
58
+ })
59
+ })
@@ -0,0 +1,21 @@
1
+ import type { INestApplication } from "@nestjs/common"
2
+ import type { RequestHandler } from "express"
3
+ import type { CorrelationIds, CorrelationRunner } from "./correlation.port"
4
+ import { CORRELATION_IDS, CORRELATION_RUNNER } from "./logging.decorators"
5
+
6
+ /** Copies the identity onto the request and response, then scopes all downstream request logging. */
7
+ export const correlationMiddleware =
8
+ (ids: CorrelationIds, runner: CorrelationRunner): RequestHandler =>
9
+ (request, response, next) => {
10
+ const correlationId = ids.fromHeaders(request.headers)
11
+ Object.assign(request, { correlationId })
12
+ response.setHeader("x-request-id", correlationId)
13
+ runner.run(correlationId, next)
14
+ }
15
+
16
+ /** Installs correlation before request handling; transports read the stamped request field explicitly. */
17
+ export const applyCorrelation = (app: INestApplication): void => {
18
+ app.use(
19
+ correlationMiddleware(app.get<CorrelationIds>(CORRELATION_IDS), app.get<CorrelationRunner>(CORRELATION_RUNNER)),
20
+ )
21
+ }
@@ -0,0 +1,16 @@
1
+ /** Headers accepted at the logging boundary; repeated values are not correlation identities. */
2
+ export interface CorrelationHeaders {
3
+ /** An inbound HTTP header value. */
4
+ readonly [name: string]: string | ReadonlyArray<string> | undefined
5
+ }
6
+
7
+ /** The request identity copied explicitly into transport commands and downstream envelopes. */
8
+ export interface CorrelationRequest {
9
+ /** The inbound headers read by logging. */
10
+ readonly headers: CorrelationHeaders
11
+ /** The validated or minted correlation identity. */
12
+ readonly correlationId: string
13
+ }
14
+
15
+ /** Work entered by infrastructure with an explicit identity; its result and failure pass through unchanged. */
16
+ export type CorrelationWork<T> = () => T
@@ -0,0 +1,150 @@
1
+ import { Test } from "@nestjs/testing"
2
+ import { fakeIds } from "@starci/jest-preset"
3
+ import { DomainError } from "@modules/platform/errors"
4
+ import { correlationFromHeaders, isCorrelationId, runWithCorrelation, stampLogFields } from "./correlation.policy"
5
+
6
+ type LoggingScopeErrorCode = "LOGGING_SCOPE_FAILED"
7
+ class LoggingScopeError extends DomainError<LoggingScopeErrorCode> {}
8
+
9
+ const SUBJECT = Symbol("correlation-policy")
10
+ const FIRST_ID = "00000000-0000-4000-8000-000000000001"
11
+ const traceId = "4bf92f3577b34da6a3ce929d0e0e4736"
12
+ const spanId = "00f067aa0ba902b7"
13
+ const build = async () => {
14
+ const ids = fakeIds()
15
+ const moduleRef = await Test.createTestingModule({
16
+ providers: [
17
+ {
18
+ provide: SUBJECT,
19
+ useValue: { correlationFromHeaders, isCorrelationId, runWithCorrelation, stampLogFields },
20
+ },
21
+ ],
22
+ }).compile()
23
+ return { subject: moduleRef.get<typeof import("./correlation.policy")>(SUBJECT), ids }
24
+ }
25
+
26
+ describe("correlationFromHeaders", () => {
27
+ it("uses the validated trace id before the request id", async () => {
28
+ const { subject, ids } = await build()
29
+ expect(
30
+ subject.correlationFromHeaders(
31
+ { traceparent: `00-${traceId}-${spanId}-01`, "x-request-id": "request-1" },
32
+ ids,
33
+ ),
34
+ ).toBe(traceId)
35
+ })
36
+
37
+ it.each([`01-${traceId}-${spanId}-01`, `01-${traceId}-${spanId}-01-future-field`])(
38
+ "reads the known prefix of a higher traceparent version: %s",
39
+ async (traceparent) => {
40
+ const { subject, ids } = await build()
41
+ expect(subject.correlationFromHeaders({ traceparent, "x-request-id": "request-1" }, ids)).toBe(traceId)
42
+ },
43
+ )
44
+
45
+ it.each([
46
+ "malformed",
47
+ `ff-${traceId}-${spanId}-01`,
48
+ `ff-${traceId}-${spanId}-01-future-field`,
49
+ `00-${traceId}-${spanId}-01-extra`,
50
+ `01-${traceId}-${spanId}-01extra`,
51
+ `01-${traceId}-${spanId}-01_extra`,
52
+ `01-${traceId}-${spanId}-0`,
53
+ `00-${traceId}-${spanId}-01\n`,
54
+ `01-${traceId}-${spanId}-01\n`,
55
+ `00-${"0".repeat(32)}-${spanId}-01`,
56
+ `00-${traceId}-${"0".repeat(16)}-01`,
57
+ `00-${traceId.toUpperCase()}-${spanId}-01`,
58
+ ])("falls back to a request id when traceparent is invalid: %s", async (traceparent) => {
59
+ const { subject, ids } = await build()
60
+ expect(subject.correlationFromHeaders({ traceparent, "x-request-id": "request-1" }, ids)).toBe("request-1")
61
+ })
62
+
63
+ it("does not choose among repeated traceparent values", async () => {
64
+ const { subject, ids } = await build()
65
+ expect(
66
+ subject.correlationFromHeaders(
67
+ { traceparent: [`00-${traceId}-${spanId}-01`], "x-request-id": "request-1" },
68
+ ids,
69
+ ),
70
+ ).toBe("request-1")
71
+ })
72
+
73
+ it("accepts the bounded safe request id alphabet", async () => {
74
+ const { subject, ids } = await build()
75
+ expect(subject.correlationFromHeaders({ "x-request-id": "A-1._:z" }, ids)).toBe("A-1._:z")
76
+ expect(subject.correlationFromHeaders({ "x-request-id": "a".repeat(128) }, ids)).toHaveLength(128)
77
+ })
78
+
79
+ it.each([
80
+ { requestId: undefined },
81
+ { requestId: "" },
82
+ { requestId: "a".repeat(129) },
83
+ { requestId: "line\nbreak" },
84
+ { requestId: "request-id\n" },
85
+ { requestId: "request-id\r" },
86
+ { requestId: "unsafe value" },
87
+ { requestId: ["a", "b"] },
88
+ ])("mints through Ids for absent or unsafe request ids: %p", async ({ requestId }) => {
89
+ const { subject, ids } = await build()
90
+ expect(subject.correlationFromHeaders({ "x-request-id": requestId }, ids)).toBe(FIRST_ID)
91
+ expect(ids.issued).toEqual([FIRST_ID])
92
+ })
93
+ })
94
+
95
+ describe("logging correlation scopes", () => {
96
+ it("stamps the logger root identity outside a scope", async () => {
97
+ const { subject } = await build()
98
+ expect(subject.stampLogFields(undefined, FIRST_ID)).toEqual({ correlationId: FIRST_ID })
99
+ })
100
+
101
+ it("returns the work's result and restores a nested scope", async () => {
102
+ const { subject } = await build()
103
+ const result = subject.runWithCorrelation("outer", () => {
104
+ expect(subject.stampLogFields({ correlationId: "override", owner: "logging" }, FIRST_ID)).toEqual({
105
+ correlationId: "outer",
106
+ owner: "logging",
107
+ })
108
+ subject.runWithCorrelation("inner", () =>
109
+ expect(subject.stampLogFields({}, FIRST_ID)).toEqual({ correlationId: "inner" }),
110
+ )
111
+ expect(subject.stampLogFields({}, FIRST_ID)).toEqual({ correlationId: "outer" })
112
+ return "result"
113
+ })
114
+ expect(result).toBe("result")
115
+ expect(subject.stampLogFields({}, FIRST_ID)).toEqual({ correlationId: FIRST_ID })
116
+ })
117
+
118
+ it("restores the surrounding scope after a throw", async () => {
119
+ const { subject } = await build()
120
+ const failure = new LoggingScopeError({ code: "LOGGING_SCOPE_FAILED" })
121
+ expect(() =>
122
+ subject.runWithCorrelation("failed", () => {
123
+ throw failure
124
+ }),
125
+ ).toThrow(failure)
126
+ expect(subject.stampLogFields({}, FIRST_ID)).toEqual({ correlationId: FIRST_ID })
127
+ })
128
+
129
+ it("isolates interleaved asynchronous requests", async () => {
130
+ const { subject } = await build()
131
+ let releaseFirst: (() => void) | undefined
132
+ const firstReady = new Promise<void>((resolve) => {
133
+ releaseFirst = resolve
134
+ })
135
+ const first = subject.runWithCorrelation("first", async () => {
136
+ await firstReady
137
+ return subject.stampLogFields({}, FIRST_ID)
138
+ })
139
+ const second = subject.runWithCorrelation("second", async () => {
140
+ await Promise.resolve()
141
+ releaseFirst?.()
142
+ return subject.stampLogFields({}, FIRST_ID)
143
+ })
144
+ await expect(Promise.all([first, second])).resolves.toEqual([
145
+ { correlationId: "first" },
146
+ { correlationId: "second" },
147
+ ])
148
+ expect(subject.stampLogFields({}, FIRST_ID)).toEqual({ correlationId: FIRST_ID })
149
+ })
150
+ })
@@ -0,0 +1,42 @@
1
+ import { AsyncLocalStorage } from "node:async_hooks"
2
+ import type { Ids } from "@modules/platform/ids"
3
+ import type { CorrelationHeaders, CorrelationWork } from "./correlation.contracts"
4
+ import type { LogFields } from "./logging.port"
5
+
6
+ // Logging owns the process's sole ambient correlation store; envelopes carry identities across every other boundary.
7
+ const correlation = new AsyncLocalStorage<string>()
8
+
9
+ /** Enters a logging scope without making ambient reads part of the public logging port. */
10
+ export const runWithCorrelation = <T>(correlationId: string, work: CorrelationWork<T>): T =>
11
+ correlation.run(correlationId, work)
12
+
13
+ /** Stamps the logger's fields last so caller-supplied fields cannot replace the active identity; outside any scope the logger's own root identity stands. */
14
+ export const stampLogFields = (fields: LogFields | undefined, rootId: string): LogFields => ({
15
+ ...fields,
16
+ correlationId: correlation.getStore() ?? rootId,
17
+ })
18
+
19
+ /** Parses the known W3C prefix; newer versions keep opaque additions, while version 00 permits none. */
20
+ const traceIdOf = (traceparent: string): string | undefined => {
21
+ const match = /^(?!ff)([0-9a-f]{2})-((?!0{32})[0-9a-f]{32})-(?!0{16})[0-9a-f]{16}-[0-9a-f]{2}((?![\s\S])|-)/.exec(
22
+ traceparent,
23
+ )
24
+ if (!match) return undefined
25
+ if (match[1] === "00" && match[3] === "-") return undefined
26
+ return match[2]
27
+ }
28
+
29
+ /** Reads a valid W3C trace identity or a safe request id; malformed headers cannot enter logs. */
30
+ export const correlationFromHeaders = (headers: CorrelationHeaders, ids: Ids): string => {
31
+ const traceparent = headers["traceparent"]
32
+ if (typeof traceparent === "string") {
33
+ const traceId = traceIdOf(traceparent)
34
+ if (traceId) return traceId
35
+ }
36
+ const requestId = headers["x-request-id"]
37
+ return isCorrelationId(requestId) ? requestId : ids.next()
38
+ }
39
+
40
+ /** Accepts only bounded, header-safe identities on serialized async envelopes. */
41
+ export const isCorrelationId = (value: unknown): value is string =>
42
+ typeof value === "string" && value.length > 0 && value.length <= 128 && !/[^A-Za-z0-9._:-]/.test(value)
@@ -0,0 +1,15 @@
1
+ import type { CorrelationHeaders, CorrelationWork } from "./correlation.contracts"
2
+
3
+ /** Runs infrastructure work under a supplied identity so logging stamps its async descendants. */
4
+ export interface CorrelationRunner {
5
+ /** Restores the surrounding logging scope when the work returns or throws. */
6
+ run<T>(correlationId: string, work: CorrelationWork<T>): T
7
+ }
8
+
9
+ /** Resolves explicit request identities without exposing the logger's ambient state. */
10
+ export interface CorrelationIds {
11
+ /** Uses a valid W3C trace id, then a bounded request id, otherwise asks the Ids port. */
12
+ fromHeaders(headers: CorrelationHeaders): string
13
+ /** Mints an identity for work that starts without an inbound request. */
14
+ create(): string
15
+ }
@@ -0,0 +1,23 @@
1
+ import { Test } from "@nestjs/testing"
2
+ import { fakeIds } from "@starci/jest-preset"
3
+ import { IDS } from "@modules/platform/ids"
4
+ import { CorrelationService, createCorrelationRunner } from "./correlation.service"
5
+ import { stampLogFields } from "./correlation.policy"
6
+
7
+ describe("CorrelationService", () => {
8
+ it("resolves headers, creates identities, and runs work with an explicit identity", async () => {
9
+ const ids = fakeIds()
10
+ const moduleRef = await Test.createTestingModule({
11
+ providers: [CorrelationService, { provide: IDS, useValue: ids }],
12
+ }).compile()
13
+ const service = moduleRef.get<CorrelationService>(CorrelationService)
14
+
15
+ expect(service.fromHeaders({ "x-request-id": "request-1" })).toBe("request-1")
16
+ expect(service.fromHeaders({})).toBe("00000000-0000-4000-8000-000000000001")
17
+ expect(service.create()).toBe("00000000-0000-4000-8000-000000000002")
18
+ expect(service.run("explicit-id", () => stampLogFields({}, "root-id"))).toEqual({ correlationId: "explicit-id" })
19
+ expect(createCorrelationRunner(ids).run("bootstrap-id", () => stampLogFields({}, "root-id"))).toEqual({
20
+ correlationId: "bootstrap-id",
21
+ })
22
+ })
23
+ })
@@ -0,0 +1,28 @@
1
+ import { InjectIds } from "@modules/platform/ids"
2
+ import type { Ids } from "@modules/platform/ids"
3
+ import type { CorrelationHeaders, CorrelationWork } from "./correlation.contracts"
4
+ import { correlationFromHeaders, runWithCorrelation } from "./correlation.policy"
5
+ import type { CorrelationIds, CorrelationRunner } from "./correlation.port"
6
+
7
+ /** Owns inbound identity resolution and logging scopes; business code receives explicit identities. */
8
+ export class CorrelationService implements CorrelationIds, CorrelationRunner {
9
+ constructor(@InjectIds() private readonly ids: Ids) {}
10
+
11
+ /** Chooses an inbound identity, minting through Ids when both headers are absent or invalid. */
12
+ fromHeaders(headers: CorrelationHeaders): string {
13
+ return correlationFromHeaders(headers, this.ids)
14
+ }
15
+
16
+ /** Starts work that has no inbound identity through the deterministic Ids port. */
17
+ create(): string {
18
+ return this.ids.next()
19
+ }
20
+
21
+ /** Applies only to logging; callers still pass the identity to outgoing envelopes explicitly. */
22
+ run<T>(correlationId: string, work: CorrelationWork<T>): T {
23
+ return runWithCorrelation(correlationId, work)
24
+ }
25
+ }
26
+
27
+ /** Builds an infrastructure logging runner for bootstrap and test worlds without a Nest container. */
28
+ export const createCorrelationRunner = (ids: Ids): CorrelationRunner => new CorrelationService(ids)
@@ -1,5 +1,18 @@
1
1
  export { createJsonLogger } from "./json-logger.service"
2
- export { InjectLogger, LOGGER } from "./logging.decorators"
2
+ export {
3
+ InjectLogger,
4
+ LOGGER,
5
+ CORRELATION_IDS,
6
+ CORRELATION_RUNNER,
7
+ InjectCorrelationIds,
8
+ InjectCorrelationRunner,
9
+ } from "./logging.decorators"
10
+ export { applyCorrelation, correlationMiddleware } from "./correlation-request.policy"
11
+ export type { CorrelationIds, CorrelationRunner } from "./correlation.port"
12
+ export type { CorrelationHeaders, CorrelationRequest } from "./correlation.contracts"
13
+ export { createCorrelationRunner } from "./correlation.service"
14
+ export { isCorrelationId } from "./correlation.policy"
3
15
  export { LoggingLogEvent } from "./logging.log-events"
4
16
  export { LoggingModule } from "./logging.module"
5
17
  export type { Logger } from "./logging.port"
18
+ export { createNestLogger } from "./nest-logger.service"
@@ -1,10 +1,12 @@
1
1
  import type { Writable } from "node:stream"
2
- import { FakeClock, mock } from "@starci/jest-preset"
2
+ import { FakeClock, fakeIds, mock } from "@starci/jest-preset"
3
3
  import { CLOCK } from "@modules/platform/clock"
4
+ import { IDS } from "@modules/platform/ids"
4
5
  import { Test } from "@nestjs/testing"
5
6
  import { createJsonLogger, JsonLoggerService } from "./json-logger.service"
6
7
  import { LOG_ERR, LOG_OUT } from "./logging.decorators"
7
8
  import { LoggingLogEvent } from "./logging.log-events"
9
+ import { runWithCorrelation } from "./correlation.policy"
8
10
 
9
11
  type WrittenLine = { stream: "out" | "err"; line: string }
10
12
 
@@ -29,9 +31,10 @@ const build = async () => {
29
31
  { provide: CLOCK, useValue: clock },
30
32
  { provide: LOG_OUT, useValue: out },
31
33
  { provide: LOG_ERR, useValue: err },
34
+ { provide: IDS, useValue: fakeIds() },
32
35
  ],
33
36
  }).compile()
34
- return { logger: moduleRef.get(JsonLoggerService), clock, written }
37
+ return { logger: moduleRef.get<JsonLoggerService>(JsonLoggerService), clock, written }
35
38
  }
36
39
 
37
40
  describe("JsonLoggerService", () => {
@@ -41,7 +44,7 @@ describe("JsonLoggerService", () => {
41
44
  expect(written).toEqual([
42
45
  {
43
46
  stream: "out",
44
- line: `${JSON.stringify({ personId: "p-1", level: "info", event: "cart.opened", time: "2026-05-06T07:08:09.000Z" })}\n`,
47
+ line: `${JSON.stringify({ personId: "p-1", correlationId: "00000000-0000-4000-8000-000000000001", level: "info", event: "cart.opened", time: "2026-05-06T07:08:09.000Z" })}\n`,
45
48
  },
46
49
  ])
47
50
  })
@@ -54,6 +57,7 @@ describe("JsonLoggerService", () => {
54
57
  level: "warn",
55
58
  event: LoggingLogEvent.StartupFailed,
56
59
  time: "2026-05-06T07:08:10.000Z",
60
+ correlationId: "00000000-0000-4000-8000-000000000001",
57
61
  })
58
62
  expect(written[0]?.stream).toBe("err")
59
63
  })
@@ -72,6 +76,7 @@ describe("JsonLoggerService error details", () => {
72
76
  level: "error",
73
77
  event: "db.failed",
74
78
  time: "2026-05-06T07:08:09.000Z",
79
+ correlationId: "00000000-0000-4000-8000-000000000001",
75
80
  })
76
81
  expect(written[0]?.stream).toBe("err")
77
82
  })
@@ -88,6 +93,7 @@ describe("JsonLoggerService error details", () => {
88
93
  level: "error",
89
94
  event: "db.failed",
90
95
  time: "2026-05-06T07:08:09.000Z",
96
+ correlationId: "00000000-0000-4000-8000-000000000001",
91
97
  })
92
98
  })
93
99
 
@@ -130,12 +136,39 @@ describe("createJsonLogger", () => {
130
136
  const lines: Array<string> = []
131
137
  jest.spyOn(process.stdout, "write").mockImplementation((chunk) => lines.push(`out:${String(chunk)}`) > 0)
132
138
  jest.spyOn(process.stderr, "write").mockImplementation((chunk) => lines.push(`err:${String(chunk)}`) > 0)
133
- const logger = createJsonLogger(new FakeClock("2026-05-06T07:08:09.000Z"))
139
+ const logger = createJsonLogger(new FakeClock("2026-05-06T07:08:09.000Z"), fakeIds())
134
140
  logger.info(LoggingLogEvent.ServerStarted)
135
141
  logger.warn(LoggingLogEvent.StartupFailed)
136
142
  expect(lines).toEqual([
137
- `out:${JSON.stringify({ level: "info", event: LoggingLogEvent.ServerStarted, time: "2026-05-06T07:08:09.000Z" })}\n`,
138
- `err:${JSON.stringify({ level: "warn", event: LoggingLogEvent.StartupFailed, time: "2026-05-06T07:08:09.000Z" })}\n`,
143
+ `out:${JSON.stringify({ correlationId: "00000000-0000-4000-8000-000000000001", level: "info", event: LoggingLogEvent.ServerStarted, time: "2026-05-06T07:08:09.000Z" })}\n`,
144
+ `err:${JSON.stringify({ correlationId: "00000000-0000-4000-8000-000000000001", level: "warn", event: LoggingLogEvent.StartupFailed, time: "2026-05-06T07:08:09.000Z" })}\n`,
145
+ ])
146
+ })
147
+ })
148
+
149
+ describe("JsonLoggerService correlation", () => {
150
+ it("stamps every level with the active identity and refuses caller overrides", async () => {
151
+ const { logger, written } = await build()
152
+ runWithCorrelation("request-id", () => {
153
+ logger.info(LoggingLogEvent.ServerStarted, { correlationId: "override" })
154
+ logger.warn(LoggingLogEvent.StartupFailed)
155
+ logger.error(LoggingLogEvent.StartupFailed, new Error("failed"))
156
+ })
157
+ expect(written).toHaveLength(3)
158
+ expect(written.every(({ line }) => readJsonLine(line).correlationId === "request-id")).toBe(true)
159
+ })
160
+ })
161
+
162
+ describe("JsonLogger outside a correlation scope", () => {
163
+ it("stamps every line with one root identity drawn once", async () => {
164
+ const { logger, written } = await build()
165
+
166
+ logger.info("cart.opened")
167
+ logger.warn("cart.slow")
168
+
169
+ expect(written.map((entry) => entry.line)).toEqual([
170
+ `${JSON.stringify({ level: "info", event: "cart.opened", time: "2026-05-06T07:08:09.000Z", correlationId: "00000000-0000-4000-8000-000000000001" })}\n`,
171
+ `${JSON.stringify({ level: "warn", event: "cart.slow", time: "2026-05-06T07:08:09.000Z", correlationId: "00000000-0000-4000-8000-000000000001" })}\n`,
139
172
  ])
140
173
  })
141
174
  })