@zigrivers/scaffold 3.16.0 → 3.18.0

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 (385) hide show
  1. package/README.md +28 -0
  2. package/content/knowledge/backend/backend-fintech-broker-integration.md +244 -0
  3. package/content/knowledge/backend/backend-fintech-compliance.md +181 -0
  4. package/content/knowledge/backend/backend-fintech-data-modeling.md +210 -0
  5. package/content/knowledge/backend/backend-fintech-ledger.md +226 -0
  6. package/content/knowledge/backend/backend-fintech-observability.md +151 -0
  7. package/content/knowledge/backend/backend-fintech-order-lifecycle.md +213 -0
  8. package/content/knowledge/backend/backend-fintech-risk-management.md +150 -0
  9. package/content/knowledge/backend/backend-fintech-testing.md +197 -0
  10. package/content/knowledge/core/automated-review-tooling.md +10 -0
  11. package/content/knowledge/core/multi-service-api-contracts.md +634 -0
  12. package/content/knowledge/core/multi-service-architecture.md +492 -0
  13. package/content/knowledge/core/multi-service-auth.md +706 -0
  14. package/content/knowledge/core/multi-service-data-ownership.md +539 -0
  15. package/content/knowledge/core/multi-service-observability.md +545 -0
  16. package/content/knowledge/core/multi-service-resilience.md +710 -0
  17. package/content/knowledge/core/multi-service-task-decomposition.md +615 -0
  18. package/content/knowledge/core/multi-service-testing.md +728 -0
  19. package/content/methodology/backend-fintech.yml +46 -0
  20. package/content/methodology/custom-defaults.yml +6 -0
  21. package/content/methodology/deep.yml +6 -0
  22. package/content/methodology/multi-service-overlay.yml +103 -0
  23. package/content/methodology/mvp.yml +6 -0
  24. package/content/pipeline/architecture/service-ownership-map.md +83 -0
  25. package/content/pipeline/quality/cross-service-auth.md +96 -0
  26. package/content/pipeline/quality/cross-service-observability.md +104 -0
  27. package/content/pipeline/quality/integration-test-plan.md +106 -0
  28. package/content/pipeline/specification/inter-service-contracts.md +95 -0
  29. package/dist/cli/commands/adopt.cli-flags.test.js +20 -0
  30. package/dist/cli/commands/adopt.cli-flags.test.js.map +1 -1
  31. package/dist/cli/commands/adopt.d.ts.map +1 -1
  32. package/dist/cli/commands/adopt.js +11 -3
  33. package/dist/cli/commands/adopt.js.map +1 -1
  34. package/dist/cli/commands/complete.d.ts +1 -0
  35. package/dist/cli/commands/complete.d.ts.map +1 -1
  36. package/dist/cli/commands/complete.js +26 -8
  37. package/dist/cli/commands/complete.js.map +1 -1
  38. package/dist/cli/commands/dashboard.d.ts +1 -0
  39. package/dist/cli/commands/dashboard.d.ts.map +1 -1
  40. package/dist/cli/commands/dashboard.js +19 -6
  41. package/dist/cli/commands/dashboard.js.map +1 -1
  42. package/dist/cli/commands/decisions.d.ts +1 -0
  43. package/dist/cli/commands/decisions.d.ts.map +1 -1
  44. package/dist/cli/commands/decisions.js +18 -4
  45. package/dist/cli/commands/decisions.js.map +1 -1
  46. package/dist/cli/commands/info.d.ts +1 -0
  47. package/dist/cli/commands/info.d.ts.map +1 -1
  48. package/dist/cli/commands/info.js +25 -3
  49. package/dist/cli/commands/info.js.map +1 -1
  50. package/dist/cli/commands/init-from.test.d.ts +2 -0
  51. package/dist/cli/commands/init-from.test.d.ts.map +1 -0
  52. package/dist/cli/commands/init-from.test.js +315 -0
  53. package/dist/cli/commands/init-from.test.js.map +1 -0
  54. package/dist/cli/commands/init.d.ts +3 -0
  55. package/dist/cli/commands/init.d.ts.map +1 -1
  56. package/dist/cli/commands/init.js +239 -129
  57. package/dist/cli/commands/init.js.map +1 -1
  58. package/dist/cli/commands/init.test.js +20 -0
  59. package/dist/cli/commands/init.test.js.map +1 -1
  60. package/dist/cli/commands/next.d.ts +1 -0
  61. package/dist/cli/commands/next.d.ts.map +1 -1
  62. package/dist/cli/commands/next.js +40 -4
  63. package/dist/cli/commands/next.js.map +1 -1
  64. package/dist/cli/commands/next.test.js +153 -0
  65. package/dist/cli/commands/next.test.js.map +1 -1
  66. package/dist/cli/commands/reset.d.ts +1 -0
  67. package/dist/cli/commands/reset.d.ts.map +1 -1
  68. package/dist/cli/commands/reset.js +77 -29
  69. package/dist/cli/commands/reset.js.map +1 -1
  70. package/dist/cli/commands/rework.d.ts +1 -0
  71. package/dist/cli/commands/rework.d.ts.map +1 -1
  72. package/dist/cli/commands/rework.js +16 -2
  73. package/dist/cli/commands/rework.js.map +1 -1
  74. package/dist/cli/commands/run.d.ts +1 -0
  75. package/dist/cli/commands/run.d.ts.map +1 -1
  76. package/dist/cli/commands/run.js +65 -13
  77. package/dist/cli/commands/run.js.map +1 -1
  78. package/dist/cli/commands/run.test.js +255 -3
  79. package/dist/cli/commands/run.test.js.map +1 -1
  80. package/dist/cli/commands/skip.d.ts +1 -0
  81. package/dist/cli/commands/skip.d.ts.map +1 -1
  82. package/dist/cli/commands/skip.js +24 -7
  83. package/dist/cli/commands/skip.js.map +1 -1
  84. package/dist/cli/commands/status.d.ts +1 -0
  85. package/dist/cli/commands/status.d.ts.map +1 -1
  86. package/dist/cli/commands/status.js +51 -4
  87. package/dist/cli/commands/status.js.map +1 -1
  88. package/dist/cli/commands/status.test.js +130 -0
  89. package/dist/cli/commands/status.test.js.map +1 -1
  90. package/dist/cli/guards-coverage.test.d.ts +2 -0
  91. package/dist/cli/guards-coverage.test.d.ts.map +1 -0
  92. package/dist/cli/guards-coverage.test.js +26 -0
  93. package/dist/cli/guards-coverage.test.js.map +1 -0
  94. package/dist/cli/guards-integration.test.d.ts +2 -0
  95. package/dist/cli/guards-integration.test.d.ts.map +1 -0
  96. package/dist/cli/guards-integration.test.js +178 -0
  97. package/dist/cli/guards-integration.test.js.map +1 -0
  98. package/dist/cli/guards.d.ts +13 -0
  99. package/dist/cli/guards.d.ts.map +1 -0
  100. package/dist/cli/guards.js +70 -0
  101. package/dist/cli/guards.js.map +1 -0
  102. package/dist/cli/guards.test.d.ts +2 -0
  103. package/dist/cli/guards.test.d.ts.map +1 -0
  104. package/dist/cli/guards.test.js +136 -0
  105. package/dist/cli/guards.test.js.map +1 -0
  106. package/dist/cli/init-flag-families.d.ts +1 -1
  107. package/dist/cli/init-flag-families.d.ts.map +1 -1
  108. package/dist/cli/init-flag-families.js +4 -1
  109. package/dist/cli/init-flag-families.js.map +1 -1
  110. package/dist/cli/init-flag-families.test.js +10 -0
  111. package/dist/cli/init-flag-families.test.js.map +1 -1
  112. package/dist/cli/shutdown.d.ts +2 -3
  113. package/dist/cli/shutdown.d.ts.map +1 -1
  114. package/dist/cli/shutdown.js +14 -11
  115. package/dist/cli/shutdown.js.map +1 -1
  116. package/dist/cli/shutdown.test.js +2 -4
  117. package/dist/cli/shutdown.test.js.map +1 -1
  118. package/dist/config/schema.d.ts +12122 -288
  119. package/dist/config/schema.d.ts.map +1 -1
  120. package/dist/config/schema.js +74 -79
  121. package/dist/config/schema.js.map +1 -1
  122. package/dist/config/schema.test.js +230 -1
  123. package/dist/config/schema.test.js.map +1 -1
  124. package/dist/config/validators/backend.d.ts +4 -0
  125. package/dist/config/validators/backend.d.ts.map +1 -0
  126. package/dist/config/validators/backend.js +14 -0
  127. package/dist/config/validators/backend.js.map +1 -0
  128. package/dist/config/validators/browser-extension.d.ts +4 -0
  129. package/dist/config/validators/browser-extension.d.ts.map +1 -0
  130. package/dist/config/validators/browser-extension.js +24 -0
  131. package/dist/config/validators/browser-extension.js.map +1 -0
  132. package/dist/config/validators/cli.d.ts +4 -0
  133. package/dist/config/validators/cli.d.ts.map +1 -0
  134. package/dist/config/validators/cli.js +14 -0
  135. package/dist/config/validators/cli.js.map +1 -0
  136. package/dist/config/validators/data-pipeline.d.ts +4 -0
  137. package/dist/config/validators/data-pipeline.d.ts.map +1 -0
  138. package/dist/config/validators/data-pipeline.js +14 -0
  139. package/dist/config/validators/data-pipeline.js.map +1 -0
  140. package/dist/config/validators/game.d.ts +4 -0
  141. package/dist/config/validators/game.d.ts.map +1 -0
  142. package/dist/config/validators/game.js +14 -0
  143. package/dist/config/validators/game.js.map +1 -0
  144. package/dist/config/validators/index.d.ts +7 -0
  145. package/dist/config/validators/index.d.ts.map +1 -0
  146. package/dist/config/validators/index.js +27 -0
  147. package/dist/config/validators/index.js.map +1 -0
  148. package/dist/config/validators/library.d.ts +4 -0
  149. package/dist/config/validators/library.d.ts.map +1 -0
  150. package/dist/config/validators/library.js +25 -0
  151. package/dist/config/validators/library.js.map +1 -0
  152. package/dist/config/validators/ml.d.ts +4 -0
  153. package/dist/config/validators/ml.d.ts.map +1 -0
  154. package/dist/config/validators/ml.js +31 -0
  155. package/dist/config/validators/ml.js.map +1 -0
  156. package/dist/config/validators/mobile-app.d.ts +4 -0
  157. package/dist/config/validators/mobile-app.d.ts.map +1 -0
  158. package/dist/config/validators/mobile-app.js +14 -0
  159. package/dist/config/validators/mobile-app.js.map +1 -0
  160. package/dist/config/validators/registry.test.d.ts +2 -0
  161. package/dist/config/validators/registry.test.d.ts.map +1 -0
  162. package/dist/config/validators/registry.test.js +26 -0
  163. package/dist/config/validators/registry.test.js.map +1 -0
  164. package/dist/config/validators/research.d.ts +4 -0
  165. package/dist/config/validators/research.d.ts.map +1 -0
  166. package/dist/config/validators/research.js +24 -0
  167. package/dist/config/validators/research.js.map +1 -0
  168. package/dist/config/validators/research.test.d.ts +2 -0
  169. package/dist/config/validators/research.test.d.ts.map +1 -0
  170. package/dist/config/validators/research.test.js +44 -0
  171. package/dist/config/validators/research.test.js.map +1 -0
  172. package/dist/config/validators/types.d.ts +19 -0
  173. package/dist/config/validators/types.d.ts.map +1 -0
  174. package/dist/config/validators/types.js +2 -0
  175. package/dist/config/validators/types.js.map +1 -0
  176. package/dist/config/validators/validators.test.d.ts +2 -0
  177. package/dist/config/validators/validators.test.d.ts.map +1 -0
  178. package/dist/config/validators/validators.test.js +25 -0
  179. package/dist/config/validators/validators.test.js.map +1 -0
  180. package/dist/config/validators/web-app.d.ts +4 -0
  181. package/dist/config/validators/web-app.d.ts.map +1 -0
  182. package/dist/config/validators/web-app.js +31 -0
  183. package/dist/config/validators/web-app.js.map +1 -0
  184. package/dist/core/assembly/context-gatherer.d.ts.map +1 -1
  185. package/dist/core/assembly/context-gatherer.js +4 -2
  186. package/dist/core/assembly/context-gatherer.js.map +1 -1
  187. package/dist/core/assembly/cross-reads.d.ts +61 -0
  188. package/dist/core/assembly/cross-reads.d.ts.map +1 -0
  189. package/dist/core/assembly/cross-reads.js +190 -0
  190. package/dist/core/assembly/cross-reads.js.map +1 -0
  191. package/dist/core/assembly/cross-reads.test.d.ts +2 -0
  192. package/dist/core/assembly/cross-reads.test.d.ts.map +1 -0
  193. package/dist/core/assembly/cross-reads.test.js +497 -0
  194. package/dist/core/assembly/cross-reads.test.js.map +1 -0
  195. package/dist/core/assembly/overlay-loader-structural.test.d.ts +2 -0
  196. package/dist/core/assembly/overlay-loader-structural.test.d.ts.map +1 -0
  197. package/dist/core/assembly/overlay-loader-structural.test.js +173 -0
  198. package/dist/core/assembly/overlay-loader-structural.test.js.map +1 -0
  199. package/dist/core/assembly/overlay-loader.d.ts +19 -3
  200. package/dist/core/assembly/overlay-loader.d.ts.map +1 -1
  201. package/dist/core/assembly/overlay-loader.js +135 -4
  202. package/dist/core/assembly/overlay-loader.js.map +1 -1
  203. package/dist/core/assembly/overlay-loader.test.js +204 -1
  204. package/dist/core/assembly/overlay-loader.test.js.map +1 -1
  205. package/dist/core/assembly/overlay-resolver.d.ts +9 -2
  206. package/dist/core/assembly/overlay-resolver.d.ts.map +1 -1
  207. package/dist/core/assembly/overlay-resolver.js +32 -1
  208. package/dist/core/assembly/overlay-resolver.js.map +1 -1
  209. package/dist/core/assembly/overlay-resolver.test.js +135 -17
  210. package/dist/core/assembly/overlay-resolver.test.js.map +1 -1
  211. package/dist/core/assembly/overlay-state-resolver.d.ts +9 -0
  212. package/dist/core/assembly/overlay-state-resolver.d.ts.map +1 -1
  213. package/dist/core/assembly/overlay-state-resolver.js +43 -2
  214. package/dist/core/assembly/overlay-state-resolver.js.map +1 -1
  215. package/dist/core/assembly/overlay-state-resolver.test.js +321 -0
  216. package/dist/core/assembly/overlay-state-resolver.test.js.map +1 -1
  217. package/dist/core/assembly/update-mode.d.ts +1 -0
  218. package/dist/core/assembly/update-mode.d.ts.map +1 -1
  219. package/dist/core/assembly/update-mode.js +17 -9
  220. package/dist/core/assembly/update-mode.js.map +1 -1
  221. package/dist/core/dependency/eligibility.d.ts +10 -1
  222. package/dist/core/dependency/eligibility.d.ts.map +1 -1
  223. package/dist/core/dependency/eligibility.js +19 -1
  224. package/dist/core/dependency/eligibility.js.map +1 -1
  225. package/dist/core/dependency/eligibility.test.js +82 -0
  226. package/dist/core/dependency/eligibility.test.js.map +1 -1
  227. package/dist/core/dependency/graph.d.ts +4 -1
  228. package/dist/core/dependency/graph.d.ts.map +1 -1
  229. package/dist/core/dependency/graph.js +7 -1
  230. package/dist/core/dependency/graph.js.map +1 -1
  231. package/dist/core/dependency/graph.test.js +48 -0
  232. package/dist/core/dependency/graph.test.js.map +1 -1
  233. package/dist/core/pipeline/global-steps.d.ts +7 -0
  234. package/dist/core/pipeline/global-steps.d.ts.map +1 -0
  235. package/dist/core/pipeline/global-steps.js +18 -0
  236. package/dist/core/pipeline/global-steps.js.map +1 -0
  237. package/dist/core/pipeline/resolver.d.ts +1 -0
  238. package/dist/core/pipeline/resolver.d.ts.map +1 -1
  239. package/dist/core/pipeline/resolver.js +54 -7
  240. package/dist/core/pipeline/resolver.js.map +1 -1
  241. package/dist/core/pipeline/resolver.test.js +51 -1
  242. package/dist/core/pipeline/resolver.test.js.map +1 -1
  243. package/dist/core/pipeline/types.d.ts +5 -1
  244. package/dist/core/pipeline/types.d.ts.map +1 -1
  245. package/dist/e2e/cross-service-references.test.d.ts +22 -0
  246. package/dist/e2e/cross-service-references.test.d.ts.map +1 -0
  247. package/dist/e2e/cross-service-references.test.js +230 -0
  248. package/dist/e2e/cross-service-references.test.js.map +1 -0
  249. package/dist/e2e/multi-service-pipeline.test.d.ts +10 -0
  250. package/dist/e2e/multi-service-pipeline.test.d.ts.map +1 -0
  251. package/dist/e2e/multi-service-pipeline.test.js +185 -0
  252. package/dist/e2e/multi-service-pipeline.test.js.map +1 -0
  253. package/dist/e2e/project-type-overlays.test.js +68 -0
  254. package/dist/e2e/project-type-overlays.test.js.map +1 -1
  255. package/dist/e2e/service-execution.test.d.ts +15 -0
  256. package/dist/e2e/service-execution.test.d.ts.map +1 -0
  257. package/dist/e2e/service-execution.test.js +219 -0
  258. package/dist/e2e/service-execution.test.js.map +1 -0
  259. package/dist/e2e/service-manifest.test.d.ts +19 -0
  260. package/dist/e2e/service-manifest.test.d.ts.map +1 -0
  261. package/dist/e2e/service-manifest.test.js +166 -0
  262. package/dist/e2e/service-manifest.test.js.map +1 -0
  263. package/dist/project/__frozen-schemas__/schema-v3.9.2.d.ts +224 -224
  264. package/dist/project/frontmatter.d.ts.map +1 -1
  265. package/dist/project/frontmatter.js +11 -0
  266. package/dist/project/frontmatter.js.map +1 -1
  267. package/dist/project/frontmatter.test.js +71 -0
  268. package/dist/project/frontmatter.test.js.map +1 -1
  269. package/dist/state/completion.d.ts +1 -1
  270. package/dist/state/completion.d.ts.map +1 -1
  271. package/dist/state/completion.js +10 -8
  272. package/dist/state/completion.js.map +1 -1
  273. package/dist/state/decision-logger.d.ts +3 -2
  274. package/dist/state/decision-logger.d.ts.map +1 -1
  275. package/dist/state/decision-logger.js +12 -11
  276. package/dist/state/decision-logger.js.map +1 -1
  277. package/dist/state/ensure-v3-migration.d.ts +9 -0
  278. package/dist/state/ensure-v3-migration.d.ts.map +1 -0
  279. package/dist/state/ensure-v3-migration.js +35 -0
  280. package/dist/state/ensure-v3-migration.js.map +1 -0
  281. package/dist/state/lock-manager.d.ts +5 -4
  282. package/dist/state/lock-manager.d.ts.map +1 -1
  283. package/dist/state/lock-manager.js +11 -11
  284. package/dist/state/lock-manager.js.map +1 -1
  285. package/dist/state/rework-manager.d.ts +1 -2
  286. package/dist/state/rework-manager.d.ts.map +1 -1
  287. package/dist/state/rework-manager.js +4 -5
  288. package/dist/state/rework-manager.js.map +1 -1
  289. package/dist/state/state-manager.d.ts +25 -1
  290. package/dist/state/state-manager.d.ts.map +1 -1
  291. package/dist/state/state-manager.js +86 -12
  292. package/dist/state/state-manager.js.map +1 -1
  293. package/dist/state/state-manager.test.js +278 -0
  294. package/dist/state/state-manager.test.js.map +1 -1
  295. package/dist/state/state-migration-v3.d.ts +22 -0
  296. package/dist/state/state-migration-v3.d.ts.map +1 -0
  297. package/dist/state/state-migration-v3.js +82 -0
  298. package/dist/state/state-migration-v3.js.map +1 -0
  299. package/dist/state/state-migration-v3.test.d.ts +2 -0
  300. package/dist/state/state-migration-v3.test.d.ts.map +1 -0
  301. package/dist/state/state-migration-v3.test.js +196 -0
  302. package/dist/state/state-migration-v3.test.js.map +1 -0
  303. package/dist/state/state-migration.d.ts.map +1 -1
  304. package/dist/state/state-migration.js +11 -6
  305. package/dist/state/state-migration.js.map +1 -1
  306. package/dist/state/state-migration.test.js +47 -2
  307. package/dist/state/state-migration.test.js.map +1 -1
  308. package/dist/state/state-path-resolver.d.ts +23 -0
  309. package/dist/state/state-path-resolver.d.ts.map +1 -0
  310. package/dist/state/state-path-resolver.js +36 -0
  311. package/dist/state/state-path-resolver.js.map +1 -0
  312. package/dist/state/state-path-resolver.test.d.ts +2 -0
  313. package/dist/state/state-path-resolver.test.d.ts.map +1 -0
  314. package/dist/state/state-path-resolver.test.js +78 -0
  315. package/dist/state/state-path-resolver.test.js.map +1 -0
  316. package/dist/state/state-version-dispatch.d.ts +17 -0
  317. package/dist/state/state-version-dispatch.d.ts.map +1 -0
  318. package/dist/state/state-version-dispatch.js +27 -0
  319. package/dist/state/state-version-dispatch.js.map +1 -0
  320. package/dist/state/state-version-dispatch.test.d.ts +2 -0
  321. package/dist/state/state-version-dispatch.test.d.ts.map +1 -0
  322. package/dist/state/state-version-dispatch.test.js +40 -0
  323. package/dist/state/state-version-dispatch.test.js.map +1 -0
  324. package/dist/types/config.d.ts +33 -3
  325. package/dist/types/config.d.ts.map +1 -1
  326. package/dist/types/config.test.js +62 -1
  327. package/dist/types/config.test.js.map +1 -1
  328. package/dist/types/dependency.d.ts +9 -0
  329. package/dist/types/dependency.d.ts.map +1 -1
  330. package/dist/types/frontmatter.d.ts +5 -0
  331. package/dist/types/frontmatter.d.ts.map +1 -1
  332. package/dist/types/lock.d.ts +1 -1
  333. package/dist/types/lock.d.ts.map +1 -1
  334. package/dist/types/state.d.ts +1 -1
  335. package/dist/types/state.d.ts.map +1 -1
  336. package/dist/utils/artifact-path.d.ts +19 -0
  337. package/dist/utils/artifact-path.d.ts.map +1 -0
  338. package/dist/utils/artifact-path.js +95 -0
  339. package/dist/utils/artifact-path.js.map +1 -0
  340. package/dist/utils/artifact-path.test.d.ts +2 -0
  341. package/dist/utils/artifact-path.test.d.ts.map +1 -0
  342. package/dist/utils/artifact-path.test.js +138 -0
  343. package/dist/utils/artifact-path.test.js.map +1 -0
  344. package/dist/utils/errors.d.ts +3 -1
  345. package/dist/utils/errors.d.ts.map +1 -1
  346. package/dist/utils/errors.js +21 -2
  347. package/dist/utils/errors.js.map +1 -1
  348. package/dist/utils/errors.test.js +27 -1
  349. package/dist/utils/errors.test.js.map +1 -1
  350. package/dist/utils/user-errors.d.ts +46 -0
  351. package/dist/utils/user-errors.d.ts.map +1 -0
  352. package/dist/utils/user-errors.js +76 -0
  353. package/dist/utils/user-errors.js.map +1 -0
  354. package/dist/utils/user-errors.test.d.ts +2 -0
  355. package/dist/utils/user-errors.test.d.ts.map +1 -0
  356. package/dist/utils/user-errors.test.js +74 -0
  357. package/dist/utils/user-errors.test.js.map +1 -0
  358. package/dist/validation/index.d.ts.map +1 -1
  359. package/dist/validation/index.js +16 -0
  360. package/dist/validation/index.js.map +1 -1
  361. package/dist/validation/index.test.js +48 -0
  362. package/dist/validation/index.test.js.map +1 -1
  363. package/dist/validation/state-validator.d.ts +5 -2
  364. package/dist/validation/state-validator.d.ts.map +1 -1
  365. package/dist/validation/state-validator.js +18 -20
  366. package/dist/validation/state-validator.js.map +1 -1
  367. package/dist/validation/state-validator.test.js +31 -2
  368. package/dist/validation/state-validator.test.js.map +1 -1
  369. package/dist/wizard/copy/backend.d.ts.map +1 -1
  370. package/dist/wizard/copy/backend.js +12 -0
  371. package/dist/wizard/copy/backend.js.map +1 -1
  372. package/dist/wizard/flags.d.ts +1 -0
  373. package/dist/wizard/flags.d.ts.map +1 -1
  374. package/dist/wizard/questions.d.ts.map +1 -1
  375. package/dist/wizard/questions.js +5 -1
  376. package/dist/wizard/questions.js.map +1 -1
  377. package/dist/wizard/questions.test.js +45 -2
  378. package/dist/wizard/questions.test.js.map +1 -1
  379. package/dist/wizard/wizard.d.ts +23 -0
  380. package/dist/wizard/wizard.d.ts.map +1 -1
  381. package/dist/wizard/wizard.js +85 -47
  382. package/dist/wizard/wizard.js.map +1 -1
  383. package/dist/wizard/wizard.test.js +186 -1
  384. package/dist/wizard/wizard.test.js.map +1 -1
  385. package/package.json +1 -1
@@ -0,0 +1,213 @@
1
+ ---
2
+ name: backend-fintech-order-lifecycle
3
+ description: Order state machine; fills, partial fills, cancellation; event-driven order tracking; idempotency; handling "unknown" states.
4
+ topics: [backend, fintech, orders, state-machine, fills, partial-fills, event-driven, webhooks]
5
+ ---
6
+
7
+ Orders in a trading system are long-lived, asynchronous, externally mutated objects — the exact shape of problem a disciplined state machine is built for. This doc covers the canonical states and transitions, how fills and partial fills land, why "unknown" is a real state you cannot wish away, and the reconciliation posture that keeps internal bookkeeping aligned with the broker of record.
8
+
9
+ ## Summary
10
+
11
+ An order is a state machine, not a row that gets mutated ad-hoc. The canonical states are `new → submitted → partially-filled → filled | cancelled | rejected | expired`, with two terminal-branching transitions that can fire from any state: `error` (integration failure, policy violation, unhandled exception) and `cancelled` (when a cancel request arrives before terminal). Only terminal states (`filled`, `cancelled`, `rejected`, `expired`, `error`) are final; everything else is transitional and must have a forward path. Illegal transitions (say, `filled → submitted`) must be rejected at the persistence layer, not just filtered out by a `switch` statement in application code.
12
+
13
+ Fills arrive asynchronously. The broker does not call your submit endpoint and return a filled order — submission returns an acknowledgement (or doesn't, see "unknown" below) and fills trickle in over seconds, minutes, or days via webhook, streaming API, or polling. A single order produces one or many fill events; partial fills are the norm for any non-trivial size. Each fill event carries its own broker-assigned execution id, a timestamp, a price, a quantity, and usually a running cumulative fill quantity for the order.
14
+
15
+ Idempotency is not optional at any layer that accepts broker events. Webhooks are delivered at-least-once — sometimes many-times-once during broker incidents — and a duplicate fill event absolutely must not create a duplicate fill row or double-book an execution. Every fill-ingest path goes through a dedupe-on-insert against a unique `(broker, execution_id)` key with multi-month retention, and every downstream posting into the ledger (`backend-fintech-ledger.md`) carries its own idempotency key derived from the fill.
16
+
17
+ "Unknown" is a first-class state, not an edge case. When the submit call to the broker times out, you do not know whether the order reached the exchange. Assuming failure invites duplicate submissions; assuming success invites phantom positions. The correct posture: mark the order `submit-unknown`, do not retry blind, and reconcile by querying the broker's order list with your client-order-id as the key. Only once reconciliation confirms presence or absence do you transition forward.
18
+
19
+ Every state transition produces a row in an immutable `order_events` audit table and — when money or position moves — a journal entry in the ledger. The order table itself records the current state as a projection of those events. Cross-ref: `backend-fintech-ledger.md` for the journal mechanics, `backend-fintech-broker-integration.md` for the wire-level quirks of specific broker APIs, and `backend-fintech-risk-management.md` for pre-trade checks that run before `new → submitted`.
20
+
21
+ ## Deep Guidance
22
+
23
+ ### State Diagram and Transition Matrix
24
+
25
+ The diagram below is the minimum viable state machine for equities and futures orders. Add states (`held-for-review`, `pending-cancel`, `pending-replace`) only when the broker's protocol actually distinguishes them.
26
+
27
+ ```
28
+ ┌────── reject ──────┐
29
+ │ ▼
30
+ new ── submit ──► submitted ────► rejected (terminal)
31
+ │ │
32
+ │ ├── fill (partial) ──► partially-filled
33
+ │ │ │ │
34
+ │ │ │ ├── fill (partial) ──► partially-filled
35
+ │ │ │ ├── fill (final) ──► filled (terminal)
36
+ │ │ │ ├── cancel ──► cancelled (terminal)
37
+ │ │ │ └── expire (TIF) ──► expired (terminal)
38
+ │ ├── fill (full) ──► filled (terminal)
39
+ │ ├── cancel-request ──► pending-cancel ──► cancelled (terminal)
40
+ │ └── expire ──► expired (terminal)
41
+
42
+ └── submit-timeout ──► submit-unknown ──► (reconcile) ──► submitted | rejected
43
+
44
+ any non-terminal ── integration failure ──► error (terminal, requires manual review)
45
+ ```
46
+
47
+ Transition preconditions are non-trivial. `submit` requires a pre-trade risk check pass. `cancel-request` is only legal from `submitted`, `partially-filled`, or `submit-unknown` and itself creates a transient `pending-cancel` until the broker confirms. A `fill` event for an order in `cancelled` or `expired` is possible — it's a race, not a bug — and must cause a controlled transition to `filled` or `partially-filled` with a flagged event for human review, never a silent drop.
48
+
49
+ ### Order Types and Lifecycle Variants
50
+
51
+ Order type determines which transitions are possible and when `expired` fires.
52
+
53
+ - **Market**: fills immediately at best available price; rarely lives long enough to cancel; no limit price. Usually `submitted → filled` within milliseconds, but partial fills still happen in illiquid names.
54
+ - **Limit**: buy at-or-below / sell at-or-above a price. Can rest on the book for the full Time-In-Force window. Partial fills are common.
55
+ - **Stop**: becomes a market order when the stop price is touched. Two-phase lifecycle: `submitted` (resting, no fills possible) → `triggered` (internal-only state) → market-order behavior. Store the trigger event explicitly.
56
+ - **Stop-Limit**: triggers a limit order at the trigger; can sit unfilled if the limit is never reached after trigger.
57
+ - **Trailing-Stop**: stop price moves with the market by an offset or percentage. The broker tracks the trailing stop; your system should record the initial parameters and the final trigger price when it fires.
58
+ - **OCO (One-Cancels-Other)**: two orders linked; fill on one cancels the other. Model as a parent order-group with two child orders, both in the state machine, linked by `group_id` and a `cancels_on_fill` flag.
59
+ - **Bracket**: parent entry order with two OCO children (profit-take and stop-loss). Children remain `new` until parent fills, then auto-submit. Failure to enable the children on parent fill is a common defect — exercise in integration tests.
60
+
61
+ **Time-In-Force (TIF)** codes drive the `expired` transition: `DAY` (expires at session close), `GTC` (good-till-cancelled, broker-specific max lifetime — IBKR caps at 90 days), `IOC` (immediate-or-cancel, partial fill OK, remainder cancelled instantly), `FOK` (fill-or-kill, all-or-nothing, no partial), `GTD` (good-till-date), `OPG` (at-the-open), `CLS` (at-the-close). The broker enforces TIF; your system must record it and expect `expired` fills to arrive on the next session boundary, not at the millisecond TIF ended.
62
+
63
+ ### Partial-Fill Handling
64
+
65
+ A partial fill is one execution report against an open order; cumulative fill quantity equals the sum of all prior execution quantities for that order. Two tracking strategies, both valid, only one allowed at a time:
66
+
67
+ 1. **Internal aggregation**: sum your own stored fill rows; ignore the broker's `cum_qty` field except as a cross-check.
68
+ 2. **Broker-reported cumulative**: trust the broker's `cum_qty` and `avg_px` in each execution report; don't sum locally.
69
+
70
+ Mixing produces silent drift. Pick one per integration, document it, and reconcile the other as a monitor. The internal-aggregation path is more resilient to out-of-order webhook delivery; the broker-reported path is simpler but requires correct event ordering.
71
+
72
+ **Average fill price** is volume-weighted, not arithmetic. For fills `(qty_i, price_i)`:
73
+
74
+ ```
75
+ avg_fill_price = Σ(qty_i × price_i) / Σ(qty_i)
76
+ ```
77
+
78
+ Compute in fixed-scale decimals, not float. Store the avg on the order row as a cached projection, recomputed on every fill insert within the same transaction.
79
+
80
+ **When to mark an order "done"** is subtle. An IOC or FOK that partially fills transitions to `filled` for the fill quantity and `cancelled` for the remainder — some systems model this as two terminal transitions on the same order; others as one order that ends in `partially-filled` with a `cancel_reason: 'ioc_remainder'`. Pick a convention and enforce it. A DAY order that partially fills and then reaches session close transitions `partially-filled → expired` on the session boundary.
81
+
82
+ ### Cancellation Semantics
83
+
84
+ A cancel is a *request*, not an *action*. The lifecycle is `cancel_requested → pending_cancel → (broker confirms) → cancelled` OR `(fill arrives first) → partially-filled or filled`. Brokers do not guarantee that a cancel will beat an incoming fill; the exchange decides. The cancel-replace pattern (modify price/qty of a working order) is especially fraught — most brokers do not guarantee atomicity, meaning the original can fill, the replacement can fill, or both can fill in sequence. Defensive design:
85
+
86
+ - Never treat "cancel requested" as "cancelled" in UI state — use a visually distinct `pending-cancel` badge.
87
+ - Always support a **cancel-confirmed fill race**: accept a fill event on a `pending-cancel` order, transition to `filled`/`partially-filled`, and flag the race in audit.
88
+ - For cancel-replace, prefer a cancel-then-new sequence over a broker-side `replace` when the broker doesn't guarantee atomicity (Alpaca, Tradier); use native replace only where atomicity is documented (Interactive Brokers with `OrderModify`).
89
+
90
+ ### Webhook Delivery Guarantees and Dedupe
91
+
92
+ Every major broker's webhook system is at-least-once: retries on 5xx, retries on timeout, retries on TCP reset. Many are also out-of-order under load. Idempotency is enforced by a dedupe table with the broker's execution id as the key:
93
+
94
+ ```sql
95
+ CREATE TABLE broker_events (
96
+ broker TEXT NOT NULL, -- 'alpaca' | 'ibkr' | 'tradier' | ...
97
+ event_id TEXT NOT NULL, -- broker's execution_id or event_id
98
+ event_type TEXT NOT NULL, -- 'fill' | 'cancel_confirmed' | ...
99
+ order_id UUID REFERENCES orders(id),
100
+ received_at TIMESTAMPTZ NOT NULL DEFAULT now(),
101
+ payload JSONB NOT NULL,
102
+ PRIMARY KEY (broker, event_id)
103
+ );
104
+ -- Retention: 90 days minimum, 13 months for regulated flows (SEC 17a-4)
105
+ ```
106
+
107
+ Retention must exceed the broker's maximum retry horizon plus a generous margin. Alpaca retries for ~24 hours; IBKR TWS replays on reconnect for the trading day; some crypto exchanges have no documented horizon. Ninety days is a defensible minimum; thirteen months aligns with SEC 17a-4 (`backend-fintech-compliance.md`). Purging too early lets a late duplicate slip in as "new."
108
+
109
+ ### Dedupe-on-Insert Fill Handler
110
+
111
+ ```typescript
112
+ type FillEvent = {
113
+ broker: string;
114
+ executionId: string; // broker-assigned, globally unique per broker
115
+ brokerOrderId: string;
116
+ clientOrderId: string; // your id, echoed by the broker
117
+ side: 'buy' | 'sell';
118
+ quantity: Decimal;
119
+ price: Decimal;
120
+ executedAt: string; // ISO 8601, broker's clock
121
+ cumQty: Decimal;
122
+ avgPx: Decimal;
123
+ receivedAt: string; // your ingestion clock
124
+ };
125
+
126
+ async function ingestFill(evt: FillEvent): Promise<void> {
127
+ await db.tx(async (tx) => {
128
+ // 1. Dedupe — INSERT ... ON CONFLICT DO NOTHING on (broker, event_id)
129
+ const inserted = await tx.query(
130
+ `INSERT INTO broker_events (broker, event_id, event_type, order_id, payload)
131
+ VALUES ($1, $2, 'fill',
132
+ (SELECT id FROM orders WHERE client_order_id = $3), $4)
133
+ ON CONFLICT (broker, event_id) DO NOTHING
134
+ RETURNING event_id`,
135
+ [evt.broker, evt.executionId, evt.clientOrderId, evt]
136
+ );
137
+ if (inserted.rowCount === 0) return; // duplicate — silent success
138
+
139
+ // 2. Insert fill row; transition order state; recompute avg price
140
+ await tx.query(
141
+ `INSERT INTO fills (order_id, broker_execution_id, qty_minor, price_minor,
142
+ executed_at, received_at, broker_ts, storage_ts)
143
+ SELECT o.id, $1, $2, $3, $4, $5, $4, now()
144
+ FROM orders o WHERE o.client_order_id = $6`,
145
+ [evt.executionId, evt.quantity, evt.price,
146
+ evt.executedAt, evt.receivedAt, evt.clientOrderId]
147
+ );
148
+ await transitionOrder(tx, evt.clientOrderId, 'fill', evt);
149
+
150
+ // 3. Post ledger entry (idempotent on executionId)
151
+ await postFillToLedger(tx, evt);
152
+ });
153
+ }
154
+ ```
155
+
156
+ Dedupe happens inside the transaction — not in a prior `SELECT` — so two concurrent webhook deliveries from the broker's retry queue cannot both win the race.
157
+
158
+ ### Reconciliation Query
159
+
160
+ On every process startup and on a scheduled cadence (every 5 minutes during market hours is typical), query the broker for all open orders and diff against the internal state machine.
161
+
162
+ ```typescript
163
+ async function reconcileOpenOrders(broker: BrokerClient): Promise<Mismatch[]> {
164
+ const [external, internal] = await Promise.all([
165
+ broker.listOrders({ status: 'open' }), // broker says these are live
166
+ db.query(
167
+ `SELECT client_order_id, state, broker_order_id
168
+ FROM orders
169
+ WHERE state IN ('submitted','partially-filled','pending-cancel','submit-unknown')`
170
+ ),
171
+ ]);
172
+
173
+ const extById = new Map(external.map(o => [o.clientOrderId, o]));
174
+ const intById = new Map(internal.rows.map(o => [o.client_order_id, o]));
175
+ const mismatches: Mismatch[] = [];
176
+
177
+ for (const [cid, ext] of extById) {
178
+ const int = intById.get(cid);
179
+ if (!int) mismatches.push({ kind: 'external-only', cid, ext });
180
+ else if (int.state === 'submit-unknown') mismatches.push({ kind: 'resolved-unknown', cid, ext });
181
+ else if (int.broker_order_id !== ext.id) mismatches.push({ kind: 'id-mismatch', cid });
182
+ }
183
+ for (const [cid, int] of intById) {
184
+ if (!extById.has(cid)) mismatches.push({ kind: 'internal-only-open', cid, int });
185
+ }
186
+ return mismatches;
187
+ }
188
+ ```
189
+
190
+ Mismatch workflows: `resolved-unknown` and `id-mismatch` auto-reconcile by pulling the broker's order detail and replaying events; `external-only` (broker has an order we never recorded) and `internal-only-open` (we think it's live, broker doesn't) go to a manual-review queue with SLA. Never auto-cancel an `external-only` order without operator approval — it may be the survivor of a different deploy.
191
+
192
+ ### Clock Drift and Timestamps
193
+
194
+ Three distinct timestamps must be recorded on every fill and every state transition, because any one alone will lie:
195
+
196
+ - **Broker timestamp** (`broker_ts`): when the broker says the event occurred. Authoritative for regulatory reporting and sequencing with exchange data. Subject to the broker's clock sync.
197
+ - **Ingestion timestamp** (`received_at`): when your webhook endpoint received the payload. Bounds network + broker-queue latency. Critical for SLA alerting.
198
+ - **Storage timestamp** (`storage_ts`): when the row was committed to your database. Authoritative for ordering within your system.
199
+
200
+ Storing only one collapses three distinct failure modes (broker clock skew, webhook queue delay, your DB commit delay) into an un-debuggable blob. When reconstructing a timeline for a trade dispute, all three are demanded. NTP-sync every host, and alarm on drift > 500ms.
201
+
202
+ ### Common Pitfalls
203
+
204
+ - **Losing fills on 5xx responses.** If your webhook endpoint returns 500 during a database outage, the broker retries — for a while. Alpaca gives up after ~24 hours; IBKR replays on TWS reconnect but not indefinitely. After the retry horizon, fills are permanently lost to the event stream and only reconciliation finds them. Never return 5xx on business-logic failures; ack with 200 and queue the event internally for retry.
205
+ - **Double-counting when switching between webhook and polling.** Running both ingest paths simultaneously without a shared dedupe table duplicates every fill. The dedupe table (`broker_events` keyed on `(broker, event_id)`) is a hard dependency of either path and both.
206
+ - **State stored as strings with no enum.** `order.status = "Filled"` vs `"filled"` vs `"FILLED"` — the bug reports write themselves. Use a database enum or a `CHECK (state IN (...))` constraint and a typed enum in code; make illegal transitions impossible at the persistence layer.
207
+ - **Treating `submit-unknown` as `rejected`.** A developer sees a submit timeout, retries the order, and the broker happily accepts both — the customer is now long 200 shares instead of 100. Always reconcile, never retry blind.
208
+ - **Ignoring out-of-order webhook delivery.** Two partial fills arrive, but delivery order is reversed; the naive handler marks `cum_qty` going backwards. Use the broker's own sequence number or `cum_qty` as the source of truth for ordering within an order's fill stream; reject inserts that would decrease `cum_qty` without flagging.
209
+ - **Forgetting cancel-fill races.** The UI shows "cancelled" but a fill arrives 200ms later. Allow the transition, flag for review, do not silently drop. Dropping is fraud-adjacent — the customer has the position and you've hidden it.
210
+ - **Not reconciling on reconnect.** A process restart while the broker's websocket was disconnected loses every event that arrived in the gap. Every reconnect triggers a full reconciliation pass for open orders, not just a resume.
211
+ - **TIF mis-modeling on overnight sessions.** A DAY order in US equities expires at 4pm ET; a DAY order in futures expires at the session close of the specific contract (5pm CT for most CME). A GTC for IBKR auto-cancels at 90 days even if you meant "forever." Store the broker's exact TIF semantics, don't translate to your own enum.
212
+
213
+ See also `backend-fintech-ledger.md` for the journal entries every fill emits, `backend-fintech-broker-integration.md` for per-broker protocol quirks, and `backend-fintech-risk-management.md` for the pre-trade checks gating the `new → submitted` transition.
@@ -0,0 +1,150 @@
1
+ ---
2
+ name: backend-fintech-risk-management
3
+ description: Position limits, drawdown caps, circuit breakers, kill switches; pre-trade and post-trade risk checks; operational risk controls.
4
+ topics: [backend, fintech, risk, position-limits, drawdown, circuit-breakers, kill-switch, pre-trade-checks]
5
+ ---
6
+
7
+ Risk management in a trading system is the set of controls that stops a bad day from becoming a catastrophic one. It lives in two places: *before* an order goes to the broker (pre-trade checks that block) and *after* fills land (post-trade monitoring that alerts, throttles, or halts). Neither half is optional, and both are exercised continuously, not just during incidents.
8
+
9
+ ## Summary
10
+
11
+ There are two classes of controls and both are mandatory. Pre-trade checks run synchronously in the order-submission path and block an order before it ever reaches the broker wire — max order size, max position per symbol, max portfolio leverage, margin/buying-power available, restricted-symbol list (hard blocks for sanctions and compliance), and fat-finger price sanity (reject a limit order more than, say, 10% away from last trade). Post-trade checks run continuously on the position and P&L stream and throttle or halt new orders when thresholds are breached — realized vs unrealized P&L, rolling max drawdown, velocity of losses (dollars lost per minute), and position concentration (no more than X% of NAV in any single name).
12
+
13
+ Pre-trade checks live on the hot path and must be fast (single-digit milliseconds); post-trade checks run out of band on a stream of fill events and ledger updates. Pre-trade is the *hard* layer — if a check fails, the order does not go. Post-trade is the *soft* layer that can escalate through severity tiers (warning → throttle → halt → kill) as conditions degrade. Both layers feed the same observability and audit surface (`backend-fintech-observability.md`).
14
+
15
+ Every risk control is parameterized per account, per strategy, and per account segment. Retail accounts have different leverage limits than institutional; a market-making strategy has different velocity caps than a buy-and-hold rebalancer. Hardcoded limits are a defect — configuration lives in a versioned, auditable store (Postgres, Consul, LaunchDarkly) with blast-radius-aware deploys.
16
+
17
+ A kill switch is a single boolean read on the order-submission path that, when set, halts all new orders instantly. It must be triggerable both manually (one click, one operator, logged and alerted) and automatically on hard threshold breach (e.g., drawdown exceeds 5% of NAV intraday). Deactivation is dual-control: two operators must sign off. The kill switch has a defined "safe state" — does activation merely halt new orders, or does it also flatten open positions? Different products pick different answers; the choice must be explicit, documented, and tested.
18
+
19
+ Operational risk — the risk from the system itself, not the market — is controlled via canary accounts (new strategy changes go live on a throwaway sub-account first), staged rollouts (ramp from 1% → 10% → 100% of size over hours), and dry-run/shadow modes where the new logic runs on live data but never submits orders. Cross-ref: `backend-fintech-order-lifecycle.md` for where pre-trade checks fire in the order flow, `backend-fintech-compliance.md` for regulatory-driven blocks (Reg SHO, sanctions), `backend-fintech-broker-integration.md` for how broker-enforced limits interact with ours, and `backend-fintech-testing.md` for chaos and shadow-mode test patterns.
20
+
21
+ ## Deep Guidance
22
+
23
+ ### Pre-Trade Check Pipeline
24
+
25
+ Pre-trade checks run as an ordered pipeline, fail-fast, with every outcome (pass, block, bypass) emitting a structured audit event. Ordering matters: cheap, definitive checks first; expensive, I/O-bound checks last, so that a restricted-symbol block never waits on a margin calculation.
26
+
27
+ ```python
28
+ # pre_trade.py
29
+ CHECKS = [
30
+ check_kill_switch, # 1. global halt (microseconds, in-memory)
31
+ check_restricted_symbol, # 2. sanctions / hard list (cache lookup)
32
+ check_max_order_size, # 3. notional and quantity caps
33
+ check_fat_finger_price, # 4. limit price vs last trade (< 10% band)
34
+ check_position_limit, # 5. would this breach max position?
35
+ check_portfolio_leverage, # 6. requires full portfolio read
36
+ check_buying_power, # 7. requires margin engine call
37
+ ]
38
+
39
+ def validate(order, account, market) -> CheckResult:
40
+ for check in CHECKS:
41
+ result = check(order, account, market)
42
+ audit.record(order.client_order_id, check.__name__, result)
43
+ if result.action == BLOCK:
44
+ return result # fail fast
45
+ if result.action == BYPASS:
46
+ audit.alert("pre_trade_bypass", order, check.__name__, result.reason)
47
+ return CheckResult(action=PASS)
48
+ ```
49
+
50
+ Manual overrides ("bypass this check for this order") exist for operational reasons (e.g., a portfolio manager closing a position that would otherwise violate the concentration rule). They must be explicitly logged with operator identity, ticket reference, and the specific check bypassed — and they must page on-call, not silently drop.
51
+
52
+ ### Per-Symbol vs Per-Account vs Per-Segment Limits
53
+
54
+ Limits compose from three axes and the most restrictive wins. Per-symbol limits cap exposure to idiosyncratic risk ("no more than 50k shares of AAPL"). Per-account limits cap total exposure for a single client ("no more than $10M notional long"). Per-segment limits apply policy to a class of accounts ("retail accounts capped at 2x leverage under Reg T; institutional prime-brokered accounts can run portfolio margin up to 6–7x effective"). An order that would fit the account and segment limits but breach the per-symbol cap still rejects.
55
+
56
+ Store limits in a versioned config table, not code. Every change goes through a PR-like workflow with an approver, and activation is timestamped so post-mortems can answer "what were the limits at 14:32:07 UTC?"
57
+
58
+ ### Margin and Buying Power
59
+
60
+ Under Reg T (US retail equities), initial margin is 50% — a $100k long position requires $50k equity. Maintenance margin is 25% (FINRA floor; most brokers set 30%). Pattern-day-trader (PDT) accounts with over $25k equity get 4x intraday buying power on marginable securities; non-PDT accounts are capped at 2x. Portfolio margin (for accounts over $100k–$150k and approved) replaces the flat percentage with a risk-array calculation — typically 10–15% haircut on diversified portfolios, which yields roughly 6–7x gross leverage on balanced books.
61
+
62
+ Buying-power math must update in real time as fills land and marks move. A common and costly bug: computing buying power from the position snapshot but ignoring working orders (resting limits that could fill at any moment). The correct denominator is `cash + market_value - initial_margin_of_open_positions - initial_margin_of_working_orders`. Re-run the calculation on every fill, every cancel, and every mark-to-market tick on volatile books.
63
+
64
+ ### Drawdown Tracking
65
+
66
+ Drawdown is the peak-to-trough decline of equity over a time window. Track at least two windows: intraday (high-water mark resets at session open) and rolling N-day (e.g., 5-day and 30-day high-water marks). Intraday drawdown is the tripwire for kill-switch automation; multi-day drawdown drives strategy-level throttles.
67
+
68
+ ```python
69
+ # drawdown.py
70
+ from collections import deque
71
+
72
+ class DrawdownTracker:
73
+ def __init__(self, window_sec: int):
74
+ self.window_sec = window_sec
75
+ self.samples = deque() # (ts, equity)
76
+ self.peak = None
77
+
78
+ def update(self, ts: float, equity: float) -> float:
79
+ self.samples.append((ts, equity))
80
+ cutoff = ts - self.window_sec
81
+ while self.samples and self.samples[0][0] < cutoff:
82
+ self.samples.popleft()
83
+ self.peak = max(s[1] for s in self.samples)
84
+ return (equity - self.peak) / self.peak # negative = drawdown
85
+ ```
86
+
87
+ Intraday peak resets at the session open boundary, not on a rolling clock — reset on the *event* (market open) rather than a sliding window, so the 15:59 peak does not suppress the drawdown calculation at 09:31 the next morning.
88
+
89
+ ### Circuit Breakers: Tiered Severity
90
+
91
+ A flat "halt on any bad thing" is too blunt. Use four tiers, each with explicit trigger, action, and notification:
92
+
93
+ - **Warning** (drawdown > 1%): log, dashboard badge, no trading impact. Analyst acknowledges.
94
+ - **Throttle** (drawdown > 2.5%, loss velocity > $5k/min): reduce max order size by 50%, pause strategies tagged "high-turnover". Page on-call desk.
95
+ - **Halt** (drawdown > 4%): block all new opening orders. Closing orders still allowed (to let risk come off). Page desk + engineering on-call.
96
+ - **Kill** (drawdown > 5%, or catastrophic event): set global kill switch. Block all new orders including closes. Page desk + engineering + exec on-call.
97
+
98
+ Each tier's transition (up or down) emits an event and requires explicit operator action to step *down* in severity. Automatic de-escalation is tempting and dangerous; it hides the fact that you tripped at all.
99
+
100
+ ### Kill-Switch Implementation
101
+
102
+ The kill switch is a single atomic boolean in a low-latency store (Redis, an in-memory feature-flag service like LaunchDarkly, or a sidecar like Consul). Every order-submission path reads it before sending to the broker. Read latency budget: sub-millisecond. Cache locally in each process with a short TTL (e.g., 500ms) and a pub/sub invalidation channel so activation propagates in well under a second.
103
+
104
+ ```python
105
+ # kill_switch.py
106
+ STATES = ("disabled", "armed", "active", "recovering")
107
+ # disabled: killswitch feature off (testing environments)
108
+ # armed: normal operation, switch is off
109
+ # active: halt in effect, new orders rejected
110
+ # recovering: manual thaw; new orders allowed but flagged + rate-limited
111
+
112
+ TRANSITIONS = {
113
+ ("armed", "active"): ["manual_trigger", "auto_drawdown", "auto_loss_velocity"],
114
+ ("active", "recovering"): ["dual_control_approval"], # two operators
115
+ ("recovering", "armed"): ["dual_control_approval", "min_cooldown_elapsed"],
116
+ }
117
+ ```
118
+
119
+ Activation is one-click by any authorized operator. Deactivation requires two distinct operator identities approving within a short window (a classic dual-control pattern). Every transition writes to an append-only audit log with operator, timestamp, trigger reason, and current risk state snapshot.
120
+
121
+ The "safe state" must be defined: does activation merely halt new orders, or does it also issue flatten-all market orders? Neither is universally right. For directional strategies in liquid markets, flatten is sensible. For market-making books, flattening into a wide spread can be worse than holding. Document the choice per strategy and encode it in the kill-switch config.
122
+
123
+ ### Testing Risk Controls
124
+
125
+ Risk controls that have never been tripped in production are indistinguishable from risk controls that do not work. Test them continuously:
126
+
127
+ - **Chaos tests**: on a schedule, in a staging environment with live-like data, force a breach of each tier. Verify the expected action fires and pages the expected team. Treat a silent failure here the same as a production incident.
128
+ - **Shadow mode**: when rolling out a new check or tightening a limit, run it in shadow — the check evaluates and emits an event if it *would* have blocked, but does not actually block. Bake for days, compare the shadow block rate to expectations, then cut over.
129
+ - **Simulated bad fills**: inject fabricated fill events (through the test harness described in `backend-fintech-testing.md`) that would push position past limits, and verify the post-trade pipeline catches and escalates.
130
+
131
+ ### Operational Risk Controls
132
+
133
+ Beyond market risk, the system itself is a source of loss. A new strategy with a sign error can lose a day's P&L in minutes; a misconfigured limit can let a fat finger through. Three controls blunt this:
134
+
135
+ - **Canary accounts**: every new strategy or material change first runs on a small, clearly-labeled sub-account with tight notional caps. If the canary burns, the loss is bounded. Promote to full size only after a defined soak period and a review checkpoint.
136
+ - **Staged rollouts**: when scaling a strategy's size or rolling out a risk-check change, step through defined tiers (1% → 10% → 50% → 100% of target notional) with bake time at each. Automated rollback on deviation from expected P&L, fill rate, or error rate.
137
+ - **Dry-run / shadow modes**: the strategy runtime supports a mode where it runs on live market data, generates orders, and logs them — but the submission layer drops them on the floor. Diff the shadow's intended orders against the production strategy's actual orders; any deviation is a finding to investigate before promotion.
138
+
139
+ All three of these are first-class features in the order-submission path, not ad-hoc scripts. The feature flag determining shadow vs live is read at the same place as the kill switch, and its state is audited alongside every order.
140
+
141
+ ### Common Pitfalls
142
+
143
+ - **Lagging risk state**: pre-trade checks read position from a cache or replica that is seconds behind the true ledger. A burst of orders can all pass individually while collectively breaching. Mitigate with a per-account in-process reservation (increment the projected position at check time, decrement on reject or terminal).
144
+ - **Admin-path bypasses**: internal tools, reconciliation jobs, or "just this once" scripts that bypass the pre-trade pipeline. Every order-submitting path — human UI, strategy runtime, ops tooling, test harnesses in prod — goes through the same checks. No exceptions; enforce at the broker-integration layer (`backend-fintech-broker-integration.md`).
145
+ - **Hardcoded limits**: "max 1000 shares" embedded in a constants file. When a legitimate larger trade needs to go, someone edits the constant, deploys, and forgets to revert. Limits belong in config, per-account, auditable.
146
+ - **Kill switch with no safe-state definition**: activated during an incident, then the team argues in Slack about whether to flatten. Decide in advance; codify it.
147
+ - **Single-operator kill-switch deactivation**: a compromised or distracted operator can re-enable trading prematurely. Dual control is cheap to implement and catches real mistakes.
148
+ - **No drill cadence**: the kill switch is tested once at build time and never again. Run a kill-switch drill at least quarterly; measure activation-to-halt latency and deactivation-to-trading latency.
149
+ - **Missing working-order margin**: buying-power math that ignores resting limit orders produces over-allocation when several of those limits fill in the same second. Always include working-order initial margin in the denominator.
150
+ - **Automatic de-escalation**: circuit breakers that silently step down as conditions improve hide the fact that a tier was tripped. Require explicit operator action to re-arm, even if the underlying condition has cleared.
@@ -0,0 +1,197 @@
1
+ ---
2
+ name: backend-fintech-testing
3
+ description: Deterministic backtests; financial-accuracy tests; broker sandbox testing; regulatory edge-case coverage.
4
+ topics: [backend, fintech, testing, determinism, backtesting, sandbox, accuracy, property-based]
5
+ ---
6
+
7
+ Fintech tests have unusual requirements: bit-exact numeric accuracy, full determinism across runs and hosts, rich regulatory edge-case coverage, and realistic multi-session flows that span market-hours boundaries. A flakey fintech test is worse than no test — it hides the exact race conditions that cause real money to move incorrectly. This doc covers the patterns that keep backtests reproducible, numeric tests honest, broker integrations verifiable, and regulatory behavior exercised before it becomes an incident.
8
+
9
+ ## Summary
10
+
11
+ The single largest failure mode in fintech test suites is **non-determinism**. A test that calls `new Date()`, reads `Math.random()`, or pulls the system clock inside the code under test will pass on a developer laptop at 10:00 PT and fail in CI at 03:00 UTC — or worse, pass 999 times and fail on the thousandth when a tick happens to straddle a market-open boundary. Every fintech test must inject time, randomness, UUID generation, and any external clock. There are no exceptions to this rule: if the production code reads the clock directly, the production code is wrong, not the test.
12
+
13
+ **Numeric accuracy** is the second-largest source of bugs. Fintech tests should assert exact decimal equality, not `toBeCloseTo`. If you cannot predict the exact output to the last representable digit, you don't understand the computation — figure it out with a spreadsheet and pin the value. Test rounding modes explicitly (`ROUND_HALF_EVEN` vs `ROUND_HALF_UP` differ on 0.125 and similar ties) and test at boundary scales: sub-cent fees, multi-million-dollar positions, negative balances from wash-sale loss disallowance. See `backend-fintech-ledger.md` for the decimal and ledger invariants the tests are protecting.
14
+
15
+ **Broker integration** testing is a tiered strategy: unit tests against a mock layer, integration tests against the broker's sandbox (Alpaca paper, IBKR demo, Tradier sandbox, Tradovate demo), and contract tests that hit the real sandbox on a schedule to catch upstream breaking changes. Use record/replay (`nock`, `vcrpy`, `polly-js`, `msw` with fixtures) so CI runs are fast and hermetic, then rotate recordings on a schedule so they don't drift from the live API. See `backend-fintech-broker-integration.md` for the integration surface being tested.
16
+
17
+ **Regulatory edges** must be covered by explicit fixtures and properties: PDT-rule thresholds (Reg T, $25,000 minimum, 4th day trade within 5 business days), T+1 settlement (since May 28, 2024, US equities settle T+1, not T+2 — update old fixtures), halt/resume with opening-auction prices, corporate actions (dividends, splits, spin-offs, mergers) adjusting cost basis, partial fills and cancel-replace races, and market-closed order rejection. Property-based tests (`fast-check`, `hypothesis`, `jqwik`) are unusually well-suited here because financial invariants are algebraic ("sum of debits equals sum of credits across any window," "position after N operations equals sum of signed fills"). See `backend-fintech-compliance.md` for the rules these tests enforce and `backend-fintech-order-lifecycle.md` for the state machines they exercise.
18
+
19
+ ## Deep Guidance
20
+
21
+ ### Determinism Patterns
22
+
23
+ The non-negotiable rule: **production code never calls `new Date()`, `Date.now()`, `time.time()`, `uuid.uuid4()`, `random.random()`, or `crypto.randomUUID()` directly**. Every such call is injected via a `Clock`, `IdGenerator`, `Randomness`, or similar seam. In tests the seam is replaced by a `TestClock` (advances on command), a deterministic UUID generator (`uuidv7` seeded, or a counter), and a seeded PRNG (`seedrandom`, `numpy.random.default_rng(42)`, `java.util.Random(seed)`).
24
+
25
+ Sorting is the second silent source of flakes. Any list returned from a DB query, a broker response, or a cache must be sorted by a **deterministic tie-breaker** before being compared in tests. Prefer composite keys: `ORDER BY timestamp ASC, id ASC`. Relying on insertion order, hash order, or "whatever the DB returned" will fail on Postgres after an `ANALYZE`, on a parallel query plan, or in a different locale.
26
+
27
+ Timezone handling is the third. Every stored timestamp is UTC. Every test that involves market-hours logic pins `TZ=America/New_York` in the test harness (or uses a zone-aware library like `luxon`, `pendulum`, `java.time.ZoneId`). Run the full suite once with `TZ=UTC` and once with `TZ=Asia/Tokyo` in CI; if anything differs you have a latent bug.
28
+
29
+ ### Property-Based Testing for Financial Invariants
30
+
31
+ Property-based testing (`fast-check` in TS, `hypothesis` in Python, `jqwik` in Java, `proptest` in Rust) generates thousands of randomized inputs and asserts an invariant holds. The invariants in fintech are unusually strong, which makes properties cheap to write and high-value:
32
+
33
+ - **Ledger balance**: for any sequence of journal entries, `sum(debits) == sum(credits)`.
34
+ - **Position conservation**: `starting_position + sum(signed_fills) == ending_position` across any time window.
35
+ - **Idempotency**: posting the same event twice leaves the ledger in the same state as posting it once.
36
+ - **Commutativity where it should hold**: two non-overlapping transfers in either order produce the same final balances.
37
+ - **Non-commutativity where it should not**: a partial fill followed by a cancel differs from a cancel followed by a partial fill; test that the race is rejected, not silently reordered.
38
+ - **Rounding monotonicity**: `round(a) + round(b) <= round(a + b) + 1 ulp` for `ROUND_HALF_EVEN`.
39
+
40
+ Shrinking is the killer feature: when a property fails, the framework shrinks to a minimal counterexample. A 4-line failing test with `amount=0.125, mode=HALF_EVEN` is worth a hundred anecdotal bug reports.
41
+
42
+ ### Numeric Test Patterns
43
+
44
+ Never use floating-point for money, and never use `toBeCloseTo` / `assertAlmostEqual` in money tests — those are noise suppressors that hide off-by-one-cent bugs. Use `Decimal` (`decimal.js`, `big.js`, Python `decimal.Decimal`, Java `BigDecimal`, `rust_decimal`) and assert exact string equality on the serialized form: `expect(result.toFixed(4)).toBe("1234.5678")`.
45
+
46
+ Test rounding **modes** explicitly, not just results. `ROUND_HALF_EVEN` (banker's rounding, IEEE 754 default, required by many regulators) rounds 0.5 to the nearest even: 0.5 → 0, 1.5 → 2, 2.5 → 2. `ROUND_HALF_UP` rounds 0.5 away from zero: 0.5 → 1, 1.5 → 2, 2.5 → 3. A test that exercises 0.125 and 0.375 at 2-decimal scale will catch a mode swap.
47
+
48
+ Pin the expected values by hand calculation, not by "run it and snapshot." A snapshot just records whatever the buggy code produced the first time. Work the expected output on paper (or in a spreadsheet you check in alongside the test fixture), then assert exact match.
49
+
50
+ ### Regulatory Scenario Fixtures
51
+
52
+ Build a fixture library that exhaustively exercises the rules:
53
+
54
+ - **PDT threshold**: account at $24,999.99 attempts a 4th day trade within a 5-business-day window → rejected with `PDT_VIOLATION`. Account at $25,000.00 → allowed. Account flagged PDT then rising above threshold on the next close → still restricted until equity holds for 5 business days.
55
+ - **T+1 settlement**: a sale on Monday settles Tuesday. Friday sale → Monday settlement. Friday sale before a Monday holiday → Tuesday settlement. Generate a calendar for the current and prior year with holidays and test the arithmetic against a known-good source (NYSE calendar).
56
+ - **Corporate actions**: 2-for-1 split of a 100-share position at $50 cost basis → 200 shares at $25. Cash dividend of $0.50/share → cash credit, no position change. Spin-off with cost-basis allocation ratio → both positions adjusted. Merger with cash-and-stock consideration → realized gain on cash portion, carryover basis on stock portion.
57
+ - **Halt / resume**: order placed during LULD halt → rejected or queued per venue rules. Opening auction after resume → fills at the auction print, not the last pre-halt trade. Test that mark-to-market during a halt uses the last trade, not zero.
58
+ - **After-hours and pre-market**: market-on-close order entered at 16:15 ET → rejected. Limit order marked `extended_hours=true` entered at 07:30 ET → accepted. Order from a user whose account is not enabled for extended hours → rejected with `EXT_HOURS_NOT_ENABLED`.
59
+
60
+ ### Broker Sandbox Testing
61
+
62
+ Every major broker offers a sandbox — Alpaca Paper, IBKR Paper Trading, Tradier Sandbox, Tradovate Demo, Schwab (former TDA) Developer Sandbox. Sandboxes differ from production in important ways: instant fills regardless of liquidity, synthetic market data that may not match real quotes, relaxed rate limits (or stricter — Alpaca sandbox caps at 200 req/min vs 10,000/min in live), and simplified corporate-action handling. Write a table of documented sandbox-vs-prod behavioral differences and pin it to the integration-test README so nobody is surprised.
63
+
64
+ Auth flows also differ. Most sandboxes use a fixed long-lived token; production uses OAuth with refresh. Tests must cover both paths — fake the OAuth refresh in unit tests, exercise the real refresh in a nightly contract test.
65
+
66
+ ### Record and Replay
67
+
68
+ For CI speed and hermeticity, capture real sandbox responses once with `nock.recorder.rec()` (Node), `vcrpy` (Python), `WireMock` (Java), or `msw`'s `setupServer` with pre-baked fixtures, then replay them in CI. The recordings live next to the test file and are checked in.
69
+
70
+ **Rotation strategy** is what makes this durable: a scheduled job (weekly or monthly via GitHub Actions cron) re-runs the recording step against the live sandbox and diffs the new captures against the committed ones. A non-trivial diff opens a PR for human review — either the API changed (update expectations and code) or the sandbox changed (update fixtures only). Without rotation, fixtures silently drift and CI becomes a liar.
71
+
72
+ ### Contract Tests Against Live Brokers
73
+
74
+ Separate from replay-based tests, run a small **contract test** suite against the real sandbox on a schedule (weekly is typical, nightly for active integrations). These tests assert the shape of critical responses — field names, required fields, enum values, error codes — without asserting specific business outcomes. Failures here are early warning of broker API changes, often before the broker's own changelog is published. Tools: `pact` for consumer-driven contracts, or a hand-rolled `zod`/`pydantic`/`jsonschema` validator run against live responses.
75
+
76
+ ### Common Pitfalls
77
+
78
+ - **Tests pass locally, fail in CI**: almost always timezone (`TZ` differs) or locale (`LANG`, `LC_ALL` affect number parsing). Pin both in the test harness and in CI config.
79
+ - **Flakey market-hours tests**: the test reads the real clock and asserts "market is open." Fix by injecting a `Clock` and freezing it at a known instant inside the trading session.
80
+ - **Sandbox instant fills**: code assumes fills are instant because sandbox makes them so; production has a partial-fill / queued-order path that was never tested. Write a mock broker that delays and partially fills, separate from the sandbox.
81
+ - **Fixtures drift from schemas**: recorded responses reference fields the API no longer returns, and production code silently handles the absence incorrectly. Rotation (above) plus a strict schema validator on every response catches this.
82
+ - **UUID comparisons**: tests that assert full UUID equality on generated ids are brittle; either inject the generator or assert shape (`expect(id).toMatch(/^[0-9a-f-]{36}$/)`). Prefer injection for determinism.
83
+ - **Decimal serialization mismatches**: `Decimal("1.10")` and `Decimal("1.1")` compare equal but serialize differently; pin the canonical form in the test and assert on the serialized output.
84
+ - **Shared state between tests**: a test that uses the real file system, a singleton clock, or a module-level cache breaks parallel execution. Every fixture is constructed per-test.
85
+
86
+ ### Code Examples
87
+
88
+ Clock-injection wrapper (TypeScript, vitest):
89
+
90
+ ```typescript
91
+ // src/infra/clock.ts
92
+ export interface Clock { now(): Date; }
93
+ export const SystemClock: Clock = { now: () => new Date() };
94
+ export class TestClock implements Clock {
95
+ constructor(private current: Date) {}
96
+ now() { return new Date(this.current); }
97
+ advance(ms: number) { this.current = new Date(this.current.getTime() + ms); }
98
+ setTo(iso: string) { this.current = new Date(iso); }
99
+ }
100
+
101
+ // src/orders/submit.ts
102
+ export function submitOrder(clock: Clock, order: Order) {
103
+ if (!isMarketOpen(clock.now())) throw new Error("MARKET_CLOSED");
104
+ return { ...order, submittedAt: clock.now().toISOString() };
105
+ }
106
+
107
+ // tests/orders/submit.test.ts
108
+ import { describe, it, expect } from "vitest";
109
+ import { TestClock } from "../../src/infra/clock";
110
+ import { submitOrder } from "../../src/orders/submit";
111
+
112
+ describe("submitOrder", () => {
113
+ it("rejects before market open", () => {
114
+ const clock = new TestClock(new Date("2026-04-15T13:29:59Z")); // 09:29:59 ET
115
+ expect(() => submitOrder(clock, baseOrder)).toThrow("MARKET_CLOSED");
116
+ });
117
+ it("accepts exactly at market open", () => {
118
+ const clock = new TestClock(new Date("2026-04-15T13:30:00Z")); // 09:30:00 ET
119
+ expect(submitOrder(clock, baseOrder).submittedAt).toBe("2026-04-15T13:30:00.000Z");
120
+ });
121
+ });
122
+ ```
123
+
124
+ Property-based invariant (Python, hypothesis):
125
+
126
+ ```python
127
+ # tests/ledger/test_invariants.py
128
+ from decimal import Decimal
129
+ from hypothesis import given, strategies as st
130
+ from app.ledger import post_entry, account_balance, Ledger
131
+
132
+ amounts = st.decimals(min_value=Decimal("0.01"), max_value=Decimal("100000"), places=2)
133
+ accounts = st.sampled_from(["cash", "customer:alice", "customer:bob", "fees"])
134
+
135
+ @given(st.lists(st.tuples(accounts, accounts, amounts), min_size=0, max_size=50))
136
+ def test_double_entry_sums_to_zero(transfers):
137
+ ledger = Ledger()
138
+ for src, dst, amt in transfers:
139
+ if src == dst:
140
+ continue
141
+ post_entry(ledger, src=src, dst=dst, amount=amt)
142
+ total = sum(account_balance(ledger, a) for a in ledger.accounts)
143
+ assert total == Decimal("0"), f"ledger imbalanced by {total}"
144
+
145
+ @given(st.lists(st.tuples(accounts, accounts, amounts), min_size=1, max_size=20))
146
+ def test_posting_is_idempotent(transfers):
147
+ l1, l2 = Ledger(), Ledger()
148
+ for i, (src, dst, amt) in enumerate(transfers):
149
+ if src == dst:
150
+ continue
151
+ post_entry(l1, src=src, dst=dst, amount=amt, idempotency_key=f"k-{i}")
152
+ post_entry(l2, src=src, dst=dst, amount=amt, idempotency_key=f"k-{i}")
153
+ post_entry(l2, src=src, dst=dst, amount=amt, idempotency_key=f"k-{i}") # replay
154
+ for acct in l1.accounts:
155
+ assert account_balance(l1, acct) == account_balance(l2, acct)
156
+ ```
157
+
158
+ Decimal-precision test with exact-match assertions (TypeScript, vitest + `decimal.js`):
159
+
160
+ ```typescript
161
+ import { Decimal } from "decimal.js";
162
+ import { describe, it, expect } from "vitest";
163
+ import { computeCommission } from "../src/fees";
164
+
165
+ describe("computeCommission", () => {
166
+ // Rate 0.00125 on notional $10,000.00 = $12.50 exactly
167
+ it("computes exact commission at representable scale", () => {
168
+ const notional = new Decimal("10000.00");
169
+ const rate = new Decimal("0.00125");
170
+ expect(computeCommission(notional, rate).toFixed(4)).toBe("12.5000");
171
+ });
172
+
173
+ // ROUND_HALF_EVEN (banker's) on 0.125 at 2dp -> 0.12 (even), not 0.13
174
+ it("uses ROUND_HALF_EVEN on tie", () => {
175
+ Decimal.set({ rounding: Decimal.ROUND_HALF_EVEN });
176
+ const notional = new Decimal("100.00");
177
+ const rate = new Decimal("0.00125"); // = $0.125
178
+ expect(computeCommission(notional, rate).toDecimalPlaces(2).toFixed(2)).toBe("0.12");
179
+ });
180
+
181
+ // Contrast: ROUND_HALF_UP would give 0.13
182
+ it("differs from ROUND_HALF_UP on tie", () => {
183
+ Decimal.set({ rounding: Decimal.ROUND_HALF_UP });
184
+ const notional = new Decimal("100.00");
185
+ const rate = new Decimal("0.00125");
186
+ expect(computeCommission(notional, rate).toDecimalPlaces(2).toFixed(2)).toBe("0.13");
187
+ });
188
+ });
189
+ ```
190
+
191
+ ### Cross-References
192
+
193
+ - `backend-fintech-ledger.md` — the invariants these tests protect (double-entry, idempotency, reconciliation).
194
+ - `backend-fintech-order-lifecycle.md` — state machines exercised by order-flow tests.
195
+ - `backend-fintech-data-modeling.md` — schemas whose contracts the fixtures match.
196
+ - `backend-fintech-broker-integration.md` — the integration surface being sandboxed, recorded, and replayed.
197
+ - `backend-testing.md` — general backend testing conventions layered beneath the fintech-specific patterns above.
@@ -193,3 +193,13 @@ When external CLIs are unavailable, the degraded-mode behavior defined in the Su
193
193
  6. Never silently drop unavailable channels — always record the channel status and compensating coverage label in the review output.
194
194
 
195
195
  **Claude CLI channel:** Claude CLI handles its own auth and is generally always available. The compensating-pass mechanism applies to external CLIs (Codex, Gemini) that have an installation/auth gate. When Codex or Gemini are unavailable, compensating passes are dispatched via `claude -p` with focused prompts targeting the missing channel's strength area.
196
+
197
+ ### Auth Recovery Paths
198
+
199
+ Each external CLI has a distinct auth recovery path. Agents should surface these directly to the user rather than silently downgrading to a compensating pass:
200
+
201
+ - **Codex:** `codex login` — opens an interactive OAuth flow. After success, `codex login status` should return cleanly.
202
+ - **Gemini:** `gemini -p "hello"` — refreshes the token if expired; `NO_BROWSER=true` is required in headless environments.
203
+ - **Claude:** `claude auth login` if `claude -p` returns auth errors; rare in practice because Claude CLI tokens are long-lived.
204
+
205
+ If the user cannot complete auth recovery within the review session, treat the channel as unavailable and document the compensating pass. Never attempt to work around auth failures by embedding credentials in review prompts or by piping through alternative providers that weren't explicitly requested.