@maycombhq/atticus 0.0.0-stage → 2.0.0-beta.11

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 (509) hide show
  1. package/LICENSE +177 -0
  2. package/README.md +17 -2
  3. package/THIRD-PARTY-LICENSES.txt +2607 -0
  4. package/dist/bin.js +47 -0
  5. package/dist/chunk-23VFQHFQ.js +827 -0
  6. package/dist/chunk-26DBAGOP.js +315 -0
  7. package/dist/chunk-27EDZN52.js +115 -0
  8. package/dist/chunk-2K2ZEJVR.js +252 -0
  9. package/dist/chunk-3DDJLI7N.js +1081 -0
  10. package/dist/chunk-3G4YI4ZU.js +16 -0
  11. package/dist/chunk-3LU6YCSQ.js +3507 -0
  12. package/dist/chunk-3WU7LVTL.js +203 -0
  13. package/dist/chunk-4AYZQ3SY.js +285 -0
  14. package/dist/chunk-4ONORLK5.js +234 -0
  15. package/dist/chunk-4Q3NN7VF.js +32 -0
  16. package/dist/chunk-4SKZDI62.js +22 -0
  17. package/dist/chunk-4URR24DD.js +189 -0
  18. package/dist/chunk-4UZJOQYK.js +70 -0
  19. package/dist/chunk-4Y4FY34K.js +34 -0
  20. package/dist/chunk-4YWRU7JK.js +80 -0
  21. package/dist/chunk-52WCHTVZ.js +41 -0
  22. package/dist/chunk-55TBYJF2.js +36 -0
  23. package/dist/chunk-5ACUW753.js +240 -0
  24. package/dist/chunk-5BJFMAQU.js +32 -0
  25. package/dist/chunk-5ICB2BXH.js +33 -0
  26. package/dist/chunk-5NZSJ5ZG.js +60 -0
  27. package/dist/chunk-5P5BGG5L.js +307 -0
  28. package/dist/chunk-5RUMHLIS.js +104 -0
  29. package/dist/chunk-5TY4DTHQ.js +242 -0
  30. package/dist/chunk-5V5BJACL.js +2929 -0
  31. package/dist/chunk-6E2GJANQ.js +52 -0
  32. package/dist/chunk-6N2GUZMZ.js +471 -0
  33. package/dist/chunk-6RGIM2ZH.js +2069 -0
  34. package/dist/chunk-6V3DTQWK.js +391 -0
  35. package/dist/chunk-76WKMA4J.js +14 -0
  36. package/dist/chunk-77RSRG3A.js +104 -0
  37. package/dist/chunk-7A7IG7OA.js +2007 -0
  38. package/dist/chunk-7EFILTCI.js +67 -0
  39. package/dist/chunk-7HQOZALR.js +180 -0
  40. package/dist/chunk-7HWXYP4H.js +1217 -0
  41. package/dist/chunk-7J7UY46S.js +688 -0
  42. package/dist/chunk-7KXYMC2P.js +38 -0
  43. package/dist/chunk-7LCQ7JAM.js +55 -0
  44. package/dist/chunk-7RGCQ4VZ.js +22 -0
  45. package/dist/chunk-7RUMD56V.js +26 -0
  46. package/dist/chunk-7S3XFWOU.js +251 -0
  47. package/dist/chunk-7SZPENUP.js +32 -0
  48. package/dist/chunk-7VFVQX5K.js +105 -0
  49. package/dist/chunk-ACWQDL6T.js +37 -0
  50. package/dist/chunk-ANCI3LFS.js +25 -0
  51. package/dist/chunk-ASKDCMW5.js +275 -0
  52. package/dist/chunk-ATNG5VOZ.js +122 -0
  53. package/dist/chunk-B3QOYE74.js +58 -0
  54. package/dist/chunk-B3UHYXHU.js +41 -0
  55. package/dist/chunk-BA5DTXHQ.js +250 -0
  56. package/dist/chunk-BKFGGJKA.js +2743 -0
  57. package/dist/chunk-BQNEHB5U.js +212 -0
  58. package/dist/chunk-BUWSOQLU.js +131 -0
  59. package/dist/chunk-BX52REMA.js +134 -0
  60. package/dist/chunk-BXBCMGIQ.js +82 -0
  61. package/dist/chunk-C2YGYTU6.js +408 -0
  62. package/dist/chunk-C5JFMAXX.js +102 -0
  63. package/dist/chunk-C75W7VXX.js +42 -0
  64. package/dist/chunk-CAN3EBE5.js +1726 -0
  65. package/dist/chunk-CE4S5ZY3.js +182 -0
  66. package/dist/chunk-CRQ73DIF.js +25 -0
  67. package/dist/chunk-CZI2ACNT.js +36 -0
  68. package/dist/chunk-D7MWYJZF.js +43 -0
  69. package/dist/chunk-DDAEAZEV.js +54 -0
  70. package/dist/chunk-DGKNOPSE.js +549 -0
  71. package/dist/chunk-DQNKG4HY.js +906 -0
  72. package/dist/chunk-DS3ZEJVN.js +22 -0
  73. package/dist/chunk-DYWTQXP4.js +108 -0
  74. package/dist/chunk-E25G46RW.js +54 -0
  75. package/dist/chunk-EADP3PEN.js +450 -0
  76. package/dist/chunk-EAQFRD2B.js +430 -0
  77. package/dist/chunk-EATSD7YD.js +41 -0
  78. package/dist/chunk-EBWIOODZ.js +339 -0
  79. package/dist/chunk-EHUYLNLC.js +282 -0
  80. package/dist/chunk-EI4KQYJ3.js +1496 -0
  81. package/dist/chunk-EQOSXLK3.js +49 -0
  82. package/dist/chunk-EV4TEFOD.js +16667 -0
  83. package/dist/chunk-EW7MRMD5.js +312 -0
  84. package/dist/chunk-EZSZFI54.js +531 -0
  85. package/dist/chunk-FMRMTI7W.js +1142 -0
  86. package/dist/chunk-FOQXYMOR.js +46 -0
  87. package/dist/chunk-G3PBLW4K.js +246 -0
  88. package/dist/chunk-G7WZH54P.js +221 -0
  89. package/dist/chunk-GG6HIU74.js +211 -0
  90. package/dist/chunk-GMJXC5AL.js +27809 -0
  91. package/dist/chunk-GPLFYLGH.js +130 -0
  92. package/dist/chunk-GVFUEGGM.js +19943 -0
  93. package/dist/chunk-GWSWS264.js +130 -0
  94. package/dist/chunk-H4VJO3M2.js +229 -0
  95. package/dist/chunk-HGQZTUAC.js +415 -0
  96. package/dist/chunk-HOUUBE74.js +166 -0
  97. package/dist/chunk-HQMCHHY5.js +155 -0
  98. package/dist/chunk-HR4DVO6X.js +374 -0
  99. package/dist/chunk-HZYFLEWO.js +39 -0
  100. package/dist/chunk-IA2RKC6I.js +279 -0
  101. package/dist/chunk-IILZ7J3L.js +5548 -0
  102. package/dist/chunk-IRW64JFF.js +247 -0
  103. package/dist/chunk-IVHN2VYG.js +1526 -0
  104. package/dist/chunk-IWZPGIGT.js +856 -0
  105. package/dist/chunk-IY4PHPHT.js +15 -0
  106. package/dist/chunk-J27QGXO4.js +1247 -0
  107. package/dist/chunk-JBSD57N7.js +324 -0
  108. package/dist/chunk-JEUXCIRD.js +120 -0
  109. package/dist/chunk-JMOLB347.js +46388 -0
  110. package/dist/chunk-JQX6BSE4.js +130 -0
  111. package/dist/chunk-JRPQX2EK.js +376 -0
  112. package/dist/chunk-JUQHYTXN.js +45 -0
  113. package/dist/chunk-JUZ75OG4.js +344 -0
  114. package/dist/chunk-KIWEBM6G.js +378 -0
  115. package/dist/chunk-KLKENMWQ.js +891 -0
  116. package/dist/chunk-KQDZXGNR.js +14 -0
  117. package/dist/chunk-KQK6SQPE.js +48 -0
  118. package/dist/chunk-KQRYW7OS.js +4108 -0
  119. package/dist/chunk-KU4T2CXJ.js +116 -0
  120. package/dist/chunk-KXZR2BLL.js +53 -0
  121. package/dist/chunk-L7I5PKGD.js +915 -0
  122. package/dist/chunk-LDIARP6F.js +31 -0
  123. package/dist/chunk-LEHZTKB3.js +34 -0
  124. package/dist/chunk-LS3WR2RF.js +1071 -0
  125. package/dist/chunk-LUPW4WLK.js +526 -0
  126. package/dist/chunk-LVBPY2M6.js +406 -0
  127. package/dist/chunk-LZWVK4YY.js +169 -0
  128. package/dist/chunk-MCWWMMLN.js +48 -0
  129. package/dist/chunk-MKC7P5LD.js +32 -0
  130. package/dist/chunk-MKFYQQ5Q.js +36 -0
  131. package/dist/chunk-MKIULIUQ.js +273 -0
  132. package/dist/chunk-MMB2OHUY.js +263 -0
  133. package/dist/chunk-MQNBEO3N.js +2610 -0
  134. package/dist/chunk-MSL3CLHX.js +545 -0
  135. package/dist/chunk-MV4637YA.js +174 -0
  136. package/dist/chunk-MYKQVS5W.js +296 -0
  137. package/dist/chunk-MYOW3RPC.js +143 -0
  138. package/dist/chunk-NBGBMRIS.js +25 -0
  139. package/dist/chunk-NBQO75XT.js +287 -0
  140. package/dist/chunk-NCR24CPV.js +41 -0
  141. package/dist/chunk-NDW4ROST.js +38 -0
  142. package/dist/chunk-NELU7LSL.js +3208 -0
  143. package/dist/chunk-NG5R2WIY.js +2919 -0
  144. package/dist/chunk-NH5QH3WS.js +119 -0
  145. package/dist/chunk-NIYB5K5B.js +583 -0
  146. package/dist/chunk-NR5T46ZY.js +49 -0
  147. package/dist/chunk-NX6QEEOA.js +163 -0
  148. package/dist/chunk-O6ILVBYB.js +18 -0
  149. package/dist/chunk-OFWYZH6B.js +230 -0
  150. package/dist/chunk-OHXTB6CO.js +50 -0
  151. package/dist/chunk-OJN7WME4.js +34 -0
  152. package/dist/chunk-OR5VRULZ.js +226 -0
  153. package/dist/chunk-ORZSD2KD.js +55 -0
  154. package/dist/chunk-OXVVRJDQ.js +66 -0
  155. package/dist/chunk-PQW7PYUR.js +2522 -0
  156. package/dist/chunk-Q32Y5T2U.js +27 -0
  157. package/dist/chunk-QWHTTA2X.js +130 -0
  158. package/dist/chunk-R3RVFCJ2.js +76 -0
  159. package/dist/chunk-R3VFLYZZ.js +400 -0
  160. package/dist/chunk-R6RY4Q5T.js +51 -0
  161. package/dist/chunk-R6UTCLH5.js +166 -0
  162. package/dist/chunk-RB2VSED2.js +231 -0
  163. package/dist/chunk-REWZE2XI.js +53 -0
  164. package/dist/chunk-RIEFYHXI.js +162 -0
  165. package/dist/chunk-RJDP642F.js +101 -0
  166. package/dist/chunk-RWMSJP7H.js +279 -0
  167. package/dist/chunk-S2FDPN32.js +41 -0
  168. package/dist/chunk-S5LSDXV4.js +40 -0
  169. package/dist/chunk-S7ZWCMDO.js +192 -0
  170. package/dist/chunk-SG5KY4VB.js +69 -0
  171. package/dist/chunk-SH7AZZDA.js +69 -0
  172. package/dist/chunk-SILY6ZEZ.js +104 -0
  173. package/dist/chunk-SINJKQSI.js +34 -0
  174. package/dist/chunk-SIUDWWKS.js +47 -0
  175. package/dist/chunk-SKMHS4CL.js +766 -0
  176. package/dist/chunk-SNHOLLGN.js +280 -0
  177. package/dist/chunk-SUQZGPVG.js +119 -0
  178. package/dist/chunk-T6ESD5KM.js +6115 -0
  179. package/dist/chunk-TC3KTG33.js +221 -0
  180. package/dist/chunk-TCADP67G.js +101 -0
  181. package/dist/chunk-TEBBD4XH.js +7923 -0
  182. package/dist/chunk-TGEOLIAK.js +149 -0
  183. package/dist/chunk-TQYBXDVH.js +133 -0
  184. package/dist/chunk-TV45EUGM.js +51 -0
  185. package/dist/chunk-TZXHUDI6.js +233 -0
  186. package/dist/chunk-U5PDQTLA.js +50 -0
  187. package/dist/chunk-U6YKL7SI.js +334 -0
  188. package/dist/chunk-UABFFMWH.js +2078 -0
  189. package/dist/chunk-UFBKJOWB.js +325 -0
  190. package/dist/chunk-UFTQIK2J.js +43 -0
  191. package/dist/chunk-UNFCFJMF.js +95 -0
  192. package/dist/chunk-UTR67XU2.js +164 -0
  193. package/dist/chunk-V4XKYGLK.js +405 -0
  194. package/dist/chunk-VOGCSYOI.js +16 -0
  195. package/dist/chunk-VYNZED7B.js +53 -0
  196. package/dist/chunk-WCLAOKCH.js +540 -0
  197. package/dist/chunk-WFYHOXTB.js +29 -0
  198. package/dist/chunk-WGU6NIUV.js +113 -0
  199. package/dist/chunk-WIMOJMAK.js +3702 -0
  200. package/dist/chunk-WKXOEIOY.js +184 -0
  201. package/dist/chunk-WUE6HKIU.js +790 -0
  202. package/dist/chunk-WUP77GG2.js +56 -0
  203. package/dist/chunk-XF3DATVU.js +150 -0
  204. package/dist/chunk-XJAJQGXJ.js +18 -0
  205. package/dist/chunk-XKJRPVJO.js +3898 -0
  206. package/dist/chunk-XNRPCI23.js +3851 -0
  207. package/dist/chunk-XOVLGBKI.js +214 -0
  208. package/dist/chunk-XPJR74MA.js +562 -0
  209. package/dist/chunk-XPWSNKF3.js +5619 -0
  210. package/dist/chunk-XR5DD5DQ.js +104 -0
  211. package/dist/chunk-XZJE6WIT.js +706 -0
  212. package/dist/chunk-XZRON6IV.js +131 -0
  213. package/dist/chunk-Y5AYBGLB.js +368 -0
  214. package/dist/chunk-YLAM4HZH.js +95 -0
  215. package/dist/chunk-YVCOQ5TZ.js +47 -0
  216. package/dist/chunk-YWOMGMZB.js +94 -0
  217. package/dist/chunk-YWZDAZMA.js +18 -0
  218. package/dist/chunk-Z2EYRUOH.js +172 -0
  219. package/dist/chunk-Z46OXTBE.js +1611 -0
  220. package/dist/chunk-ZHWSL6SR.js +45 -0
  221. package/dist/chunk-ZJQUIG3A.js +87 -0
  222. package/dist/chunk-ZKUDS7JG.js +310 -0
  223. package/dist/chunk-ZTNS3L5Z.js +1019 -0
  224. package/dist/files.json +2024 -0
  225. package/dist/fixtures/forge/README.md +19 -0
  226. package/dist/fixtures/forge/expected/discrepancies.yaml +54 -0
  227. package/dist/fixtures/forge/expected/inputs.yaml +38 -0
  228. package/dist/fixtures/forge/expected/scan.yaml +342 -0
  229. package/dist/fixtures/forge/inputs/diagrams/forge-context.drawio +23 -0
  230. package/dist/fixtures/forge/inputs/partner-specs/northwind-pay-openapi.yaml +11 -0
  231. package/dist/fixtures/forge/inputs/tickets/ado-stories.json +4 -0
  232. package/dist/fixtures/forge/inputs/tickets/closed-service-requests.json +5 -0
  233. package/dist/fixtures/forge/inputs/wiki/integration-patterns.md +11 -0
  234. package/dist/fixtures/forge/inputs/wiki/ledger-solution-spec.md +9 -0
  235. package/dist/fixtures/forge/inputs/wiki/snow-firewall-request.md +4 -0
  236. package/dist/fixtures/forge/metarepo/judgments/reaches/84af5b6af24f7ca8c23013f3.yaml +17 -0
  237. package/dist/fixtures/forge/metarepo/judgments/reaches/c3917555edebddce8cecb647.yaml +17 -0
  238. package/dist/fixtures/forge/metarepo/judgments/reaches/c5fafd6601f8ca9e34e0bbd0.yaml +17 -0
  239. package/dist/fixtures/forge/metarepo/org-pack/agents/architecture-critic.yaml +9 -0
  240. package/dist/fixtures/forge/metarepo/org-pack/agents/story-writer.yaml +10 -0
  241. package/dist/fixtures/forge/metarepo/org-pack/checklists/architecture.yaml +115 -0
  242. package/dist/fixtures/forge/metarepo/org-pack/dialects/forge.yaml +111 -0
  243. package/dist/fixtures/forge/metarepo/org-pack/gates.yaml +22 -0
  244. package/dist/fixtures/forge/metarepo/org-pack/pack.yaml +11 -0
  245. package/dist/fixtures/forge/metarepo/org-pack/patterns/A.EB.PUB.yaml +12 -0
  246. package/dist/fixtures/forge/metarepo/org-pack/patterns/D.JDBC.yaml +12 -0
  247. package/dist/fixtures/forge/metarepo/org-pack/patterns/S.DIR.BFF.yaml +12 -0
  248. package/dist/fixtures/forge/metarepo/org-pack/patterns/S.GW.SOR.yaml +13 -0
  249. package/dist/fixtures/forge/metarepo/org-pack/patterns/S.GW.VND.yaml +14 -0
  250. package/dist/fixtures/forge/metarepo/org-pack/process/change-templates/feature.md +37 -0
  251. package/dist/fixtures/forge/metarepo/org-pack/process/skills/add-forge-endpoint/SKILL.md +17 -0
  252. package/dist/fixtures/forge/metarepo/org-pack/process/snow/firewall-rule.yaml +14 -0
  253. package/dist/fixtures/forge/metarepo/org-pack/process/snow/gateway-route.yaml +13 -0
  254. package/dist/fixtures/forge/metarepo/org-pack/profiles/forge/profile.yaml +12 -0
  255. package/dist/fixtures/forge/metarepo/org-pack/profiles/forge/repos.manifest.yaml +44 -0
  256. package/dist/fixtures/forge/metarepo/org-pack/roles.yaml +14 -0
  257. package/dist/fixtures/forge/metarepo/org-pack/runtime.yaml +14 -0
  258. package/dist/fixtures/forge/metarepo/org-pack/sources.yaml +3 -0
  259. package/dist/fixtures/forge/repos/forge-events/topics/payments.initiated.yaml +4 -0
  260. package/dist/fixtures/forge/repos/forge-events/topics/transfers.completed.yaml +4 -0
  261. package/dist/fixtures/forge/repos/forge-gateway/proxies/ledger-v1/apiproxy/ledger-v1.xml +3 -0
  262. package/dist/fixtures/forge/repos/forge-gateway/proxies/ledger-v1/apiproxy/policies/SpikeArrest.xml +3 -0
  263. package/dist/fixtures/forge/repos/forge-gateway/proxies/ledger-v1/apiproxy/policies/VerifyOAuth.xml +4 -0
  264. package/dist/fixtures/forge/repos/forge-gateway/proxies/ledger-v1/apiproxy/proxies/default.xml +14 -0
  265. package/dist/fixtures/forge/repos/forge-gateway/proxies/ledger-v1/apiproxy/targets/ledger.xml +5 -0
  266. package/dist/fixtures/forge/repos/forge-infra/alerts/ledger-api.yaml +5 -0
  267. package/dist/fixtures/forge/repos/forge-infra/k8s/ledger-api.yaml +15 -0
  268. package/dist/fixtures/forge/repos/forge-infra/k8s/portal-bff.yaml +7 -0
  269. package/dist/fixtures/forge/repos/forge-infra/terraform/ledger-db.tf +9 -0
  270. package/dist/fixtures/forge/repos/forge-ledger/api/ledger-openapi.yaml +31 -0
  271. package/dist/fixtures/forge/repos/forge-ledger/forge.yaml +4 -0
  272. package/dist/fixtures/forge/repos/forge-ledger/src/main/java/forge/ledger/LedgerEndpoints.java +13 -0
  273. package/dist/fixtures/forge/repos/forge-ledger/src/main/java/forge/ledger/LedgerEvents.java +11 -0
  274. package/dist/fixtures/forge/repos/forge-ledger/src/main/java/forge/ledger/NorthwindClient.java +10 -0
  275. package/dist/fixtures/forge/repos/forge-ledger/src/main/resources/application.yml +3 -0
  276. package/dist/fixtures/forge/repos/forge-ledger/src/main/resources/db/migration/V1__create_account.sql +5 -0
  277. package/dist/fixtures/forge/repos/forge-ledger/src/main/resources/db/migration/V2__create_entry.sql +7 -0
  278. package/dist/fixtures/forge/repos/forge-portal-bff/pom.xml +1 -0
  279. package/dist/fixtures/forge/repos/forge-portal-bff/src/main/java/forge/portal/AuditPublisher.java +12 -0
  280. package/dist/fixtures/forge/repos/forge-portal-bff/src/main/java/forge/portal/BalanceController.java +16 -0
  281. package/dist/fixtures/forge/repos/forge-portal-bff/src/main/java/forge/portal/LedgerClient.java +13 -0
  282. package/dist/fixtures/forge/repos/forge-portal-bff/src/main/java/forge/portal/PaymentEvents.java +13 -0
  283. package/dist/fixtures/forge/repos/forge-portal-bff/src/main/resources/application.yml +8 -0
  284. package/dist/fixtures/forge/repos/forge-portal-ui/package.json +1 -0
  285. package/dist/fixtures/forge/repos/forge-portal-ui/src/api/balance.ts +5 -0
  286. package/dist/fixtures/forge/repos/forge-reports-job/src/main/java/forge/reports/DailyEntriesJob.java +19 -0
  287. package/dist/fixtures/forge/repos/forge-reports-job/src/main/resources/application.yml +3 -0
  288. package/dist/fixtures/forge-change/README.md +41 -0
  289. package/dist/fixtures/forge-change/expected/findings.yaml +301 -0
  290. package/dist/fixtures/forge-change/expected/stories.yaml +101 -0
  291. package/dist/fixtures/forge-change/judgments/personal-data/0b5f3dbf5257a22275d8c74b.yaml +18 -0
  292. package/dist/fixtures/forge-change/judgments/personal-data/2759a66770134c417440bf2d.yaml +18 -0
  293. package/dist/fixtures/forge-change/judgments/personal-data/31b1ea2f14d0737f1aca79fd.yaml +18 -0
  294. package/dist/fixtures/forge-change/judgments/personal-data/49afd70c08228e65ad6ca1ed.yaml +18 -0
  295. package/dist/fixtures/forge-change/judgments/personal-data/761cabcb8a39f5e5866d2dd6.yaml +18 -0
  296. package/dist/fixtures/forge-change/judgments/personal-data/adae902b90cc8ffa0c765145.yaml +18 -0
  297. package/dist/fixtures/forge-change/judgments/personal-data/ba11bb933105ec9493e065dd.yaml +18 -0
  298. package/dist/fixtures/forge-change/judgments/personal-data/bdbcd1cdac3021c68063c3e7.yaml +18 -0
  299. package/dist/fixtures/forge-change/openspec/changes/add-transfer-limit/brief.md +35 -0
  300. package/dist/fixtures/forge-change/openspec/changes/add-transfer-limit/change.yaml +120 -0
  301. package/dist/fixtures/forge-change/openspec/changes/add-transfer-limit/data.md +18 -0
  302. package/dist/fixtures/forge-change/openspec/changes/add-transfer-limit/design.md +29 -0
  303. package/dist/fixtures/forge-change/openspec/changes/add-transfer-limit/interfaces.md +17 -0
  304. package/dist/fixtures/forge-change/openspec/changes/add-transfer-limit/mappings.md +26 -0
  305. package/dist/fixtures/forge-change/openspec/changes/add-transfer-limit/proposal.md +35 -0
  306. package/dist/fixtures/forge-change/openspec/changes/add-transfer-limit/snow.md +12 -0
  307. package/dist/fixtures/forge-change/openspec/changes/add-transfer-limit/specs/transfer-limits/spec.md +19 -0
  308. package/dist/fixtures/forge-change/openspec/changes/add-transfer-limit/stories/s1-refuse-over-limit.md +17 -0
  309. package/dist/fixtures/forge-change/openspec/changes/add-transfer-limit/stories/s2-show-remaining.md +16 -0
  310. package/dist/fixtures/forge-change/openspec/changes/add-transfer-limit/stories/s3-audit-refusals.md +18 -0
  311. package/dist/fixtures/forge-change/openspec/changes/add-transfer-limit/stories/s4-velocity-check.md +16 -0
  312. package/dist/fixtures/forge-change/openspec/changes/add-transfer-limit/stories.md +8 -0
  313. package/dist/fixtures/forge-change/openspec/changes/add-transfer-limit/tasks.md +12 -0
  314. package/dist/fixtures/forge-change/openspec/changes/add-transfer-limit/ux.md +7 -0
  315. package/dist/fixtures/forge-change/org-pack/agents/code-and-test-critic.yaml +13 -0
  316. package/dist/fixtures/forge-change/org-pack/agents/complete-critic.yaml +12 -0
  317. package/dist/fixtures/forge-change/org-pack/agents/safe-critic.yaml +12 -0
  318. package/dist/fixtures/forge-change/org-pack/agents/ship-critic.yaml +11 -0
  319. package/dist/fixtures/forge-change/org-pack/agents/true-critic.yaml +11 -0
  320. package/dist/fixtures/forge-change/org-pack/checklists/review.yaml +54 -0
  321. package/dist/fixtures/forge-change/org-pack/gates.yaml +25 -0
  322. package/dist/fixtures/forge-change/org-pack/teams/team-ledger/checklists/ledger-team.yaml +227 -0
  323. package/dist/fixtures/forge-change/recordings/complete-critic/2ad84f84bf39c1d5412697e69fe23034d6983ec45a00314bf0975e627589a3c5.json +12 -0
  324. package/dist/fixtures/forge-change/recordings/complete-critic/417c98d8e54a4387d0fd93441c49f44c2868958739070d855c1dce0c41e24db3.json +12 -0
  325. package/dist/fixtures/forge-change/recordings/complete-critic/50203ee22e9cb3ef047295edb6538fe7d5695e1e4b139f3c929013dc6ce47021.json +12 -0
  326. package/dist/fixtures/forge-change/recordings/complete-critic/89f18d67f5d9c325bf35e2a9e020f9ddb6ee13b8523315037f0905cc57c040b1.json +12 -0
  327. package/dist/fixtures/forge-change/recordings/complete-critic/b4097867d26ae8eadc8b9efa8cdd86897013358035f0b40086c299a59c4ba121.json +12 -0
  328. package/dist/fixtures/forge-change/recordings/safe-critic/302d06f49664dd67ad2e21ab9cf74c1b0f7d3f750709c6ed8eff2b70983274e2.json +12 -0
  329. package/dist/fixtures/forge-change/recordings/safe-critic/32f1049c9a5ab4a759543290db4644ec6e221cc45fdfdcf06193837c6a8ce0b9.json +12 -0
  330. package/dist/fixtures/forge-change/recordings/safe-critic/4e4a84bfc04b9f26ca4250319a879901ff7a1c081c71e46a117675fee05026b0.json +12 -0
  331. package/dist/fixtures/forge-change/recordings/safe-critic/604af0fcdd6df6b76208fa88305fdc15c37606c2a4d1ae816477e46ac8c2dd04.json +12 -0
  332. package/dist/fixtures/forge-change/recordings/safe-critic/f0bf50af11f14c50d07904f0dffe1c8c20fb0cd89770645a8824c1d30a443955.json +12 -0
  333. package/dist/fixtures/forge-change/recordings/ship-critic/350954b58833b2c04880c67db45a1b328b7fd541d9f0d943b63905b8875488a6.json +12 -0
  334. package/dist/fixtures/forge-change/recordings/ship-critic/4af95c6c96a206777cec9de1f9c2d8c55a36ddc9b8db2f81d6eb83217b835efa.json +12 -0
  335. package/dist/fixtures/forge-change/recordings/ship-critic/76c34522a49cf7587a4e5689a5030d384603fa780b8aba97d27815cbc829683e.json +12 -0
  336. package/dist/fixtures/forge-change/recordings/ship-critic/a3b2cd1ace48587e5994333db03b34c3a0f9dec4f1932f1730ddfb687f6918c8.json +12 -0
  337. package/dist/fixtures/forge-change/recordings/ship-critic/de288a4fa64d0fc20890f9a787080ce6eac5203b7b9b72d884df11e8cf111835.json +12 -0
  338. package/dist/fixtures/forge-change/recordings/story-writer/1ddb00eac2aaf91a5ec5d9dfd220116be9f75aec6546a4a3a3b050ed89af2864.json +12 -0
  339. package/dist/packages/assistant-kit/frameworks-kit/README.md +86 -0
  340. package/dist/packages/assistant-kit/frameworks-kit/checklist.md +11 -0
  341. package/dist/packages/assistant-kit/frameworks-kit/examples/tide-lantern.yaml +56 -0
  342. package/dist/packages/assistant-kit/frameworks-kit/examples/tide-platform.yaml +43 -0
  343. package/dist/packages/assistant-kit/frameworks-kit/prompts/write-framework-model.md +12 -0
  344. package/dist/packages/assistant-kit/kit/README.md +98 -0
  345. package/dist/packages/assistant-kit/kit/c2-reference.md +334 -0
  346. package/dist/packages/assistant-kit/kit/checklist.md +39 -0
  347. package/dist/packages/assistant-kit/kit/examples/house-http-client.yaml +102 -0
  348. package/dist/packages/assistant-kit/kit/examples/tide-apigee.yaml +56 -0
  349. package/dist/packages/assistant-kit/kit/examples/tide-apim.yaml +32 -0
  350. package/dist/packages/assistant-kit/kit/examples/tide-kong.yaml +44 -0
  351. package/dist/packages/assistant-kit/kit/prompts/write-dialect.md +34 -0
  352. package/dist/packages/assistant-kit/kit/worked-example.md +198 -0
  353. package/dist/packages/conformance/sample/README.md +4 -0
  354. package/dist/packages/conformance/sample/inputs/notes.md +3 -0
  355. package/dist/packages/conformance/sample/inputs/systems.csv +3 -0
  356. package/dist/packages/conformance/sample/repo/api/greeter.proto +6 -0
  357. package/dist/packages/conformance/sample/repo/api/openapi.yaml +10 -0
  358. package/dist/packages/conformance/sample/repo/api/schema.graphql +3 -0
  359. package/dist/packages/conformance/sample/repo/db/V1__create_greeting.sql +4 -0
  360. package/dist/packages/conformance/sample/repo/package.json +7 -0
  361. package/dist/packages/conformance/sample/repo/src/main/java/sample/HelloController.java +10 -0
  362. package/dist/packages/conformance/sample/repo/web/app.ts +4 -0
  363. package/dist/packages/evals/datasets/critic-lenses.yaml +62 -0
  364. package/dist/packages/evals/datasets/dialect-writer.yaml +14 -0
  365. package/dist/packages/evals/datasets/feature-namer.yaml +34 -0
  366. package/dist/packages/evals/datasets/glossary-definer.yaml +25 -0
  367. package/dist/packages/evals/datasets/story-writer.yaml +13 -0
  368. package/dist/packages/evals/datasets/template-writer.yaml +16 -0
  369. package/dist/packages/evals/datasets/test-writer.yaml +18 -0
  370. package/dist/packages/executors/reference/reference-executor.mjs +233 -0
  371. package/dist/packages/extractors/dialects/argocd.yaml +45 -0
  372. package/dist/packages/extractors/dialects/fixtures/jaxrs/accounts-service/src/main/java/forge/accounts/AccountResource.java +47 -0
  373. package/dist/packages/extractors/dialects/fixtures/jaxrs/accounts-service/src/main/java/forge/accounts/AuditClient.java +12 -0
  374. package/dist/packages/extractors/dialects/fixtures/jaxrs/accounts-service/src/main/java/forge/accounts/LedgerClient.java +17 -0
  375. package/dist/packages/extractors/dialects/fixtures/micronaut/statements-service/src/main/java/forge/statements/LedgerClient.java +14 -0
  376. package/dist/packages/extractors/dialects/fixtures/micronaut/statements-service/src/main/java/forge/statements/RatesClient.java +11 -0
  377. package/dist/packages/extractors/dialects/fixtures/micronaut/statements-service/src/main/java/forge/statements/StatementController.java +36 -0
  378. package/dist/packages/extractors/dialects/fixtures/micronaut/statements-service/src/main/resources/application.yml +7 -0
  379. package/dist/packages/extractors/dialects/fixtures/quarkus-reactive-messaging/fraud-service/src/main/java/forge/fraud/FraudScorer.java +27 -0
  380. package/dist/packages/extractors/dialects/fixtures/quarkus-reactive-messaging/fraud-service/src/main/resources/application.properties +4 -0
  381. package/dist/packages/extractors/dialects/fixtures/terraform-aws/forge-aws-infra/envs/prod/api.tf +38 -0
  382. package/dist/packages/extractors/dialects/fixtures/terraform-aws/forge-aws-infra/envs/prod/main.tf +95 -0
  383. package/dist/packages/extractors/dialects/fixtures/terraform-aws/forge-aws-infra/envs/prod/terraform.tfvars +1 -0
  384. package/dist/packages/extractors/dialects/fixtures/terraform-aws/forge-aws-infra/envs/prod/variables.tf +4 -0
  385. package/dist/packages/extractors/dialects/fixtures/terraform-azure/forge-azure-infra/main.tf +102 -0
  386. package/dist/packages/extractors/dialects/fixtures/terraform-gcp/forge-gcp-infra/main.tf +51 -0
  387. package/dist/packages/extractors/dialects/harness.yaml +101 -0
  388. package/dist/packages/extractors/dialects/jaxrs.yaml +155 -0
  389. package/dist/packages/extractors/dialects/micronaut.yaml +207 -0
  390. package/dist/packages/extractors/dialects/quarkus-reactive-messaging.yaml +158 -0
  391. package/dist/packages/extractors/dialects/tekton.yaml +71 -0
  392. package/dist/packages/extractors/dialects/terraform-aws.yaml +267 -0
  393. package/dist/packages/extractors/dialects/terraform-azure.yaml +212 -0
  394. package/dist/packages/extractors/dialects/terraform-gcp.yaml +109 -0
  395. package/dist/packages/extractors/grammars/README.md +11 -0
  396. package/dist/packages/extractors/grammars/tree-sitter-groovy.LICENSE +21 -0
  397. package/dist/packages/extractors/grammars/tree-sitter-groovy.wasm +0 -0
  398. package/dist/packages/extractors/grammars/vendored.json +11 -0
  399. package/dist/packages/learn/lessons/architect-diagrams-and-code.yaml +69 -0
  400. package/dist/packages/learn/lessons/business-analyst-story-bar.yaml +94 -0
  401. package/dist/packages/learn/lessons/delivery-lead-release-readiness.yaml +44 -0
  402. package/dist/packages/learn/lessons/designer-features-in-user-words.yaml +57 -0
  403. package/dist/packages/learn/lessons/developer-find-your-way.yaml +60 -0
  404. package/dist/packages/learn/lessons/everyone-first-hour.yaml +74 -0
  405. package/dist/packages/learn/lessons/platform-lead-coverage-and-defences.yaml +58 -0
  406. package/dist/packages/learn/lessons/product-owner-first-change.yaml +167 -0
  407. package/dist/packages/learn/lessons/qa-tests-and-trace.yaml +53 -0
  408. package/dist/packages/learn/lessons/team-lead-initiative.yaml +146 -0
  409. package/dist/packages/naysayer/constitution/constitution.yaml +305 -0
  410. package/dist/packages/naysayer/starter/review.yaml +256 -0
  411. package/dist/packages/sdk/npm/index.js +10097 -0
  412. package/dist/packages/speckit/rules/drift.yaml +78 -0
  413. package/dist/packages/taskrun/rules/conform.yaml +38 -0
  414. package/dist/packages/testgen/rules/trace.yaml +49 -0
  415. package/dist/packages/ui/dist/assets/elk-api-DDup23Us.js +2 -0
  416. package/dist/packages/ui/dist/assets/elk-worker.min--dAJroGl.js +6401 -0
  417. package/dist/packages/ui/dist/assets/elk-worker.min-DdYmS5kP.js +2 -0
  418. package/dist/packages/ui/dist/assets/ibm-plex-sans-cyrillic-400-normal-BTotfTJu.woff +0 -0
  419. package/dist/packages/ui/dist/assets/ibm-plex-sans-cyrillic-400-normal-DZqxrq2p.woff2 +0 -0
  420. package/dist/packages/ui/dist/assets/ibm-plex-sans-cyrillic-500-normal-ByOcLdNv.woff +0 -0
  421. package/dist/packages/ui/dist/assets/ibm-plex-sans-cyrillic-500-normal-CocWQlwt.woff2 +0 -0
  422. package/dist/packages/ui/dist/assets/ibm-plex-sans-cyrillic-600-normal-71GNu3SW.woff2 +0 -0
  423. package/dist/packages/ui/dist/assets/ibm-plex-sans-cyrillic-600-normal-BGq0mW3O.woff +0 -0
  424. package/dist/packages/ui/dist/assets/ibm-plex-sans-cyrillic-ext-400-normal-Dsrv2Tcn.woff +0 -0
  425. package/dist/packages/ui/dist/assets/ibm-plex-sans-cyrillic-ext-400-normal-g30qAdWV.woff2 +0 -0
  426. package/dist/packages/ui/dist/assets/ibm-plex-sans-cyrillic-ext-500-normal-Cs5J6C77.woff2 +0 -0
  427. package/dist/packages/ui/dist/assets/ibm-plex-sans-cyrillic-ext-500-normal-DB5PtV2g.woff +0 -0
  428. package/dist/packages/ui/dist/assets/ibm-plex-sans-cyrillic-ext-600-normal-Bz0x94Yp.woff +0 -0
  429. package/dist/packages/ui/dist/assets/ibm-plex-sans-cyrillic-ext-600-normal-DUMzJB7m.woff2 +0 -0
  430. package/dist/packages/ui/dist/assets/ibm-plex-sans-greek-400-normal-D9ESIMu3.woff +0 -0
  431. package/dist/packages/ui/dist/assets/ibm-plex-sans-greek-400-normal-_efipK4i.woff2 +0 -0
  432. package/dist/packages/ui/dist/assets/ibm-plex-sans-greek-500-normal-CuWXN6rf.woff +0 -0
  433. package/dist/packages/ui/dist/assets/ibm-plex-sans-greek-500-normal-JMMifIXV.woff2 +0 -0
  434. package/dist/packages/ui/dist/assets/ibm-plex-sans-greek-600-normal-D-CqTdkO.woff +0 -0
  435. package/dist/packages/ui/dist/assets/ibm-plex-sans-greek-600-normal-DzTrcv_p.woff2 +0 -0
  436. package/dist/packages/ui/dist/assets/ibm-plex-sans-latin-400-normal-CDDApCn2.woff2 +0 -0
  437. package/dist/packages/ui/dist/assets/ibm-plex-sans-latin-400-normal-CYLoc0-x.woff +0 -0
  438. package/dist/packages/ui/dist/assets/ibm-plex-sans-latin-500-normal-6ng42L7E.woff2 +0 -0
  439. package/dist/packages/ui/dist/assets/ibm-plex-sans-latin-500-normal-BgVn5rGT.woff +0 -0
  440. package/dist/packages/ui/dist/assets/ibm-plex-sans-latin-600-normal-Cu4Hd6ag.woff +0 -0
  441. package/dist/packages/ui/dist/assets/ibm-plex-sans-latin-600-normal-CuJfVYMP.woff2 +0 -0
  442. package/dist/packages/ui/dist/assets/ibm-plex-sans-latin-ext-400-normal-C5H60-Va.woff2 +0 -0
  443. package/dist/packages/ui/dist/assets/ibm-plex-sans-latin-ext-400-normal-RBey6euL.woff +0 -0
  444. package/dist/packages/ui/dist/assets/ibm-plex-sans-latin-ext-500-normal-D0aIdm-b.woff +0 -0
  445. package/dist/packages/ui/dist/assets/ibm-plex-sans-latin-ext-500-normal-DakdToA3.woff2 +0 -0
  446. package/dist/packages/ui/dist/assets/ibm-plex-sans-latin-ext-600-normal-DIrixKbi.woff +0 -0
  447. package/dist/packages/ui/dist/assets/ibm-plex-sans-latin-ext-600-normal-DOrvGEcy.woff2 +0 -0
  448. package/dist/packages/ui/dist/assets/ibm-plex-sans-vietnamese-400-normal-DG4YqDda.woff2 +0 -0
  449. package/dist/packages/ui/dist/assets/ibm-plex-sans-vietnamese-400-normal-fK1oJ5dG.woff +0 -0
  450. package/dist/packages/ui/dist/assets/ibm-plex-sans-vietnamese-500-normal-BEb3_waV.woff +0 -0
  451. package/dist/packages/ui/dist/assets/ibm-plex-sans-vietnamese-500-normal-e4dixQRQ.woff2 +0 -0
  452. package/dist/packages/ui/dist/assets/ibm-plex-sans-vietnamese-600-normal-DgdngZtN.woff +0 -0
  453. package/dist/packages/ui/dist/assets/ibm-plex-sans-vietnamese-600-normal-DpPYBSTl.woff2 +0 -0
  454. package/dist/packages/ui/dist/assets/index-C0JEkXyN.css +1 -0
  455. package/dist/packages/ui/dist/assets/index-mTOWhQZm.js +29 -0
  456. package/dist/packages/ui/dist/assets/jetbrains-mono-cyrillic-400-normal-BEIGL1Tu.woff2 +0 -0
  457. package/dist/packages/ui/dist/assets/jetbrains-mono-cyrillic-400-normal-ugxPyKxw.woff +0 -0
  458. package/dist/packages/ui/dist/assets/jetbrains-mono-cyrillic-500-normal-DJqRU3vO.woff +0 -0
  459. package/dist/packages/ui/dist/assets/jetbrains-mono-cyrillic-500-normal-DmUKJPL_.woff2 +0 -0
  460. package/dist/packages/ui/dist/assets/jetbrains-mono-greek-400-normal-B9oWc5Lo.woff +0 -0
  461. package/dist/packages/ui/dist/assets/jetbrains-mono-greek-400-normal-C190GLew.woff2 +0 -0
  462. package/dist/packages/ui/dist/assets/jetbrains-mono-greek-500-normal-D7SFKleX.woff +0 -0
  463. package/dist/packages/ui/dist/assets/jetbrains-mono-greek-500-normal-JpySY46c.woff2 +0 -0
  464. package/dist/packages/ui/dist/assets/jetbrains-mono-latin-400-normal-6-qcROiO.woff +0 -0
  465. package/dist/packages/ui/dist/assets/jetbrains-mono-latin-400-normal-V6pRDFza.woff2 +0 -0
  466. package/dist/packages/ui/dist/assets/jetbrains-mono-latin-500-normal-BWZEU5yA.woff2 +0 -0
  467. package/dist/packages/ui/dist/assets/jetbrains-mono-latin-500-normal-CJOVTJB7.woff +0 -0
  468. package/dist/packages/ui/dist/assets/jetbrains-mono-latin-ext-400-normal-Bc8Ftmh3.woff2 +0 -0
  469. package/dist/packages/ui/dist/assets/jetbrains-mono-latin-ext-400-normal-fXTG6kC5.woff +0 -0
  470. package/dist/packages/ui/dist/assets/jetbrains-mono-latin-ext-500-normal-Cut-4mMH.woff2 +0 -0
  471. package/dist/packages/ui/dist/assets/jetbrains-mono-latin-ext-500-normal-ckzbgY84.woff +0 -0
  472. package/dist/packages/ui/dist/assets/jetbrains-mono-vietnamese-400-normal-CqNFfHCs.woff +0 -0
  473. package/dist/packages/ui/dist/assets/jetbrains-mono-vietnamese-500-normal-DNRqzVM1.woff +0 -0
  474. package/dist/packages/ui/dist/assets/rajdhani-devanagari-600-normal-BtY48IV_.woff +0 -0
  475. package/dist/packages/ui/dist/assets/rajdhani-devanagari-600-normal-DhS7ScYx.woff2 +0 -0
  476. package/dist/packages/ui/dist/assets/rajdhani-devanagari-700-normal-BQQOj9BB.woff2 +0 -0
  477. package/dist/packages/ui/dist/assets/rajdhani-devanagari-700-normal-DBRwWuxA.woff +0 -0
  478. package/dist/packages/ui/dist/assets/rajdhani-latin-600-normal-CXCVEoA9.woff2 +0 -0
  479. package/dist/packages/ui/dist/assets/rajdhani-latin-600-normal-Dx02qI2_.woff +0 -0
  480. package/dist/packages/ui/dist/assets/rajdhani-latin-700-normal-BPDUZo87.woff2 +0 -0
  481. package/dist/packages/ui/dist/assets/rajdhani-latin-700-normal-e0Uj8ZlN.woff +0 -0
  482. package/dist/packages/ui/dist/assets/rajdhani-latin-ext-600-normal-BleIgkPp.woff +0 -0
  483. package/dist/packages/ui/dist/assets/rajdhani-latin-ext-600-normal-C9ZjJQAt.woff2 +0 -0
  484. package/dist/packages/ui/dist/assets/rajdhani-latin-ext-700-normal-BPaQ7tlD.woff +0 -0
  485. package/dist/packages/ui/dist/assets/rajdhani-latin-ext-700-normal-S6aYESQK.woff2 +0 -0
  486. package/dist/packages/ui/dist/favicon.svg +1 -0
  487. package/dist/packages/ui/dist/index.html +15 -0
  488. package/dist/third-party.json +575 -0
  489. package/dist/vendor/@fission-ai/openspec/bin/openspec.js +85986 -0
  490. package/dist/vendor/@fission-ai/openspec/package.json +82 -0
  491. package/dist/vendor/@fission-ai/openspec/schemas/spec-driven/schema.yaml +248 -0
  492. package/dist/vendor/@fission-ai/openspec/schemas/spec-driven/templates/design.md +21 -0
  493. package/dist/vendor/@fission-ai/openspec/schemas/spec-driven/templates/proposal.md +34 -0
  494. package/dist/vendor/@fission-ai/openspec/schemas/spec-driven/templates/spec.md +13 -0
  495. package/dist/vendor/@fission-ai/openspec/schemas/spec-driven/templates/tasks.md +11 -0
  496. package/dist/vendor/tree-sitter-wasms/out/tree-sitter-c_sharp.wasm +0 -0
  497. package/dist/vendor/tree-sitter-wasms/out/tree-sitter-go.wasm +0 -0
  498. package/dist/vendor/tree-sitter-wasms/out/tree-sitter-java.wasm +0 -0
  499. package/dist/vendor/tree-sitter-wasms/out/tree-sitter-javascript.wasm +0 -0
  500. package/dist/vendor/tree-sitter-wasms/out/tree-sitter-kotlin.wasm +0 -0
  501. package/dist/vendor/tree-sitter-wasms/out/tree-sitter-php.wasm +0 -0
  502. package/dist/vendor/tree-sitter-wasms/out/tree-sitter-python.wasm +0 -0
  503. package/dist/vendor/tree-sitter-wasms/out/tree-sitter-ruby.wasm +0 -0
  504. package/dist/vendor/tree-sitter-wasms/out/tree-sitter-rust.wasm +0 -0
  505. package/dist/vendor/tree-sitter-wasms/out/tree-sitter-scala.wasm +0 -0
  506. package/dist/vendor/tree-sitter-wasms/out/tree-sitter-tsx.wasm +0 -0
  507. package/dist/vendor/tree-sitter-wasms/out/tree-sitter-typescript.wasm +0 -0
  508. package/dist/vendor/web-tree-sitter/tree-sitter.wasm +0 -0
  509. package/package.json +36 -3
@@ -0,0 +1,2522 @@
1
+ import { createRequire as __atticusCreateRequire } from "node:module";
2
+ import { fileURLToPath as __atticusFileURLToPath } from "node:url";
3
+ var require = __atticusCreateRequire(import.meta.url);
4
+ var __atticusUrl = (p) => new URL("./" + p, import.meta.url).href;
5
+ var __atticusPath = (p) => __atticusFileURLToPath(__atticusUrl(p));
6
+ import {
7
+ buildStub,
8
+ generate,
9
+ parseSpec
10
+ } from "./chunk-SKMHS4CL.js";
11
+ import {
12
+ loadPlanContext,
13
+ parseStory
14
+ } from "./chunk-EV4TEFOD.js";
15
+ import {
16
+ docOf,
17
+ fingerprint,
18
+ hopRoutes,
19
+ readRound,
20
+ roundsOf,
21
+ sectionLines,
22
+ writeFinding
23
+ } from "./chunk-T6ESD5KM.js";
24
+ import {
25
+ changeFolder
26
+ } from "./chunk-3LU6YCSQ.js";
27
+ import {
28
+ baseClonePath,
29
+ changeWorktreesDir,
30
+ readChangeRepos,
31
+ worktreePath
32
+ } from "./chunk-XZJE6WIT.js";
33
+ import {
34
+ buildContextPack,
35
+ toRunSummary
36
+ } from "./chunk-BKFGGJKA.js";
37
+ import {
38
+ AgentDefinition,
39
+ ChecklistPack,
40
+ Finding,
41
+ RecordId,
42
+ changeBranch,
43
+ external_exports,
44
+ normalizePath,
45
+ require_dist,
46
+ taskBranch,
47
+ toCanonicalYaml
48
+ } from "./chunk-JMOLB347.js";
49
+ import {
50
+ writeFileAtomic
51
+ } from "./chunk-MMB2OHUY.js";
52
+ import {
53
+ __toESM
54
+ } from "./chunk-TV45EUGM.js";
55
+
56
+ // packages/testgen/dist/generate.js
57
+ var import_yaml2 = __toESM(require_dist(), 1);
58
+ import { readdir, readFile as readFile3 } from "node:fs/promises";
59
+ import { join as join5, relative, sep } from "node:path";
60
+
61
+ // packages/testgen/dist/errors.js
62
+ var TestgenError = class extends Error {
63
+ code;
64
+ constructor(code, message) {
65
+ super(message);
66
+ this.code = code;
67
+ this.name = "TestgenError";
68
+ }
69
+ };
70
+
71
+ // packages/testgen/dist/words.js
72
+ var words = (s) => s.replace(/([a-z0-9])([A-Z])/g, "$1 $2").split(/[^A-Za-z0-9]+/).filter(Boolean);
73
+ function camel(s, max = 60) {
74
+ const w = words(s).map((x) => x.toLowerCase());
75
+ if (!w.length)
76
+ return "test";
77
+ const out = w.map((x, i) => i === 0 ? x : x.charAt(0).toUpperCase() + x.slice(1)).join("");
78
+ const id = /^[0-9]/.test(out) ? `t${out}` : out;
79
+ return id.slice(0, max);
80
+ }
81
+ function pascal(s, max = 60) {
82
+ const c = camel(s, max);
83
+ return c.charAt(0).toUpperCase() + c.slice(1);
84
+ }
85
+ function slug(s) {
86
+ return s.toLowerCase().replace(/\{[^}]*\}/g, "").replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
87
+ }
88
+ function uniqueNames(names) {
89
+ const seen = /* @__PURE__ */ new Map();
90
+ return names.map((n) => {
91
+ const k = seen.get(n) ?? 0;
92
+ seen.set(n, k + 1);
93
+ return k === 0 ? n : `${n}${k + 1}`;
94
+ });
95
+ }
96
+ var quote = (s) => s.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\r?\n/g, " ");
97
+ var oneLine = (s, max = 120) => s.replace(/[\r\n]+/g, " ").replace(/[`*]/g, "").replace(/\s+/g, " ").trim().slice(0, max);
98
+
99
+ // packages/testgen/dist/generators/types.js
100
+ function layout(repo) {
101
+ switch (repo.language) {
102
+ case "java":
103
+ return { features: "src/test/resources/features", json: "src/test/resources/atticus" };
104
+ case "node":
105
+ return { features: "test/features", json: "test/atticus" };
106
+ default:
107
+ return { features: "features", json: "atticus" };
108
+ }
109
+ }
110
+
111
+ // packages/testgen/dist/generators/generic.js
112
+ var gherkinGenerator = {
113
+ id: "gherkin",
114
+ version: "1.0.0",
115
+ generic: true,
116
+ appliesTo: () => true,
117
+ render(model, repo, ctx) {
118
+ const scenarios = repo.cases.filter((c) => c.type === "acceptance");
119
+ if (!scenarios.length)
120
+ return [];
121
+ const s = model.story;
122
+ const lines = [
123
+ `@change-${model.change} @story-${s.id}`,
124
+ `Feature: ${oneLine(s.title)}`,
125
+ ...s.asA && s.iWant && s.soThat ? [
126
+ ` As ${/^an?\s/i.test(s.asA) ? s.asA : `a ${s.asA}`}`,
127
+ ` I want ${s.iWant}`,
128
+ ` So that ${s.soThat}`
129
+ ] : [],
130
+ ...s.tracesTo ? [` Traces to ${s.tracesTo}.`] : []
131
+ ];
132
+ for (const c of scenarios) {
133
+ lines.push("");
134
+ if (c.trace)
135
+ lines.push(` # ${c.trace.file}:${c.trace.line}${c.trace.name ? ` (${c.trace.name})` : ""}`);
136
+ lines.push(` ${[`@criterion-${c.criterion}`, ...c.tags.map((t) => `@${t}`)].join(" ")}`);
137
+ lines.push(` Scenario: ${oneLine(c.name)}`);
138
+ if (c.given)
139
+ lines.push(` Given ${c.given}`);
140
+ if (c.when)
141
+ lines.push(` When ${c.when}`);
142
+ lines.push(` Then ${c.then}`);
143
+ }
144
+ return [
145
+ {
146
+ path: `${layout(repo).features}/${s.id}.feature`,
147
+ content: `${lines.join("\n")}
148
+ `,
149
+ style: "hash",
150
+ source: ctx.source,
151
+ cases: scenarios.map((c) => c.id)
152
+ }
153
+ ];
154
+ }
155
+ };
156
+ function jsonCase(c) {
157
+ const { title: _t, ...rest } = c;
158
+ void _t;
159
+ return rest;
160
+ }
161
+ var jsonGenerator = {
162
+ id: "json",
163
+ version: "1.0.0",
164
+ generic: true,
165
+ appliesTo: () => true,
166
+ render(model, repo, ctx) {
167
+ const doc = {
168
+ format: "atticus-tests/1",
169
+ change: model.change,
170
+ story: model.story,
171
+ ...repo.repo ? { repo: repo.repo } : {},
172
+ ...repo.component ? { component: repo.component } : {},
173
+ strategy: model.strategy.required,
174
+ cases: repo.cases.map(jsonCase)
175
+ };
176
+ return [
177
+ {
178
+ path: `${layout(repo).json}/${model.story.id}.tests.json`,
179
+ content: `${JSON.stringify(doc, null, 2)}
180
+ `,
181
+ style: "json",
182
+ source: ctx.source,
183
+ cases: repo.cases.map((c) => c.id)
184
+ }
185
+ ];
186
+ }
187
+ };
188
+
189
+ // packages/testgen/dist/generators/junit.js
190
+ var CODE = /^[A-Z][A-Z0-9]*_[A-Z0-9_]+$/;
191
+ var textBlock = (v) => {
192
+ const json2 = JSON.stringify(v, null, 2).replace(/\\/g, "\\\\").replace(/"""/g, '\\"""');
193
+ return `"""
194
+ ${json2.split("\n").map((l) => ` ${l}`).join("\n")}
195
+ """`;
196
+ };
197
+ function statusMatcher(status) {
198
+ if (/^\d{3}$/.test(status))
199
+ return `.statusCode(${status})`;
200
+ const d = /^([1-5])XX$/i.exec(status)?.[1];
201
+ return d ? `.statusCode(allOf(greaterThanOrEqualTo(${d}00), lessThan(${Number(d) + 1}00)))` : ".statusCode(greaterThanOrEqualTo(400))";
202
+ }
203
+ function bodyChecks(fields, body) {
204
+ const out = [];
205
+ if (body && typeof body === "object" && !Array.isArray(body)) {
206
+ for (const [k, v] of Object.entries(body))
207
+ if (typeof v === "string" && CODE.test(v))
208
+ out.push(`.body("${quote(k)}", equalTo("${quote(v)}"))`);
209
+ }
210
+ for (const f of fields)
211
+ if (!out.some((o) => o.startsWith(`.body("${f}"`)))
212
+ out.push(`.body("$", hasKey("${quote(f)}"))`);
213
+ return out;
214
+ }
215
+ function authLine(c) {
216
+ if (c.request.auth === "none")
217
+ return [];
218
+ if (c.request.auth === "no-scope")
219
+ return [
220
+ `.auth().oauth2(env("ATTICUS_TOKEN_WITHOUT_${c.request.scope.toUpperCase().replace(/[^A-Z0-9]+/g, "_")}"))`
221
+ ];
222
+ switch (c.request.authType) {
223
+ case "basic":
224
+ return [`.auth().preemptive().basic(env("ATTICUS_USER"), env("ATTICUS_PASSWORD"))`];
225
+ case "apiKey":
226
+ case "api-key":
227
+ return [`.header(env("ATTICUS_API_KEY_HEADER", "X-API-Key"), env("ATTICUS_API_KEY"))`];
228
+ case "mtls":
229
+ case "mutualTLS":
230
+ return [
231
+ `// mTLS: the client certificate comes from the test environment (RestAssured keyStore).`
232
+ ];
233
+ default:
234
+ return [`.auth().oauth2(env("ATTICUS_TOKEN"))`];
235
+ }
236
+ }
237
+ var header = (pkg) => [
238
+ `package ${pkg};`,
239
+ "",
240
+ "import static io.restassured.RestAssured.given;",
241
+ "import static org.hamcrest.Matchers.*;",
242
+ "",
243
+ "import io.restassured.http.ContentType;",
244
+ "import org.junit.jupiter.api.Assumptions;",
245
+ "import org.junit.jupiter.api.DisplayName;",
246
+ "import org.junit.jupiter.api.Test;",
247
+ ""
248
+ ];
249
+ var ENV = [
250
+ " private static String env(String name) {",
251
+ ' return env(name, "");',
252
+ " }",
253
+ "",
254
+ " private static String env(String name, String fallback) {",
255
+ " String v = System.getenv(name);",
256
+ " return v == null || v.isEmpty() ? fallback : v;",
257
+ " }"
258
+ ];
259
+ function apiMethod(c, name, baseVar) {
260
+ const lines = [` @Test`, ` @DisplayName("${quote(c.name)}")`, ` void ${name}() {`];
261
+ if (c.request.auth === "no-scope") {
262
+ const v = `ATTICUS_TOKEN_WITHOUT_${c.request.scope.toUpperCase().replace(/[^A-Z0-9]+/g, "_")}`;
263
+ lines.push(` Assumptions.assumeFalse(env("${v}").isEmpty(), "Set ${v} to a token without ${quote(c.request.scope)}.");`);
264
+ }
265
+ if (c.kind === "error" || c.why.includes("needs data"))
266
+ lines.push(` // ${c.why}`);
267
+ const hasBody = c.request.body !== void 0;
268
+ const dataVar = c.data && (c.kind === "happy" || c.kind === "error") ? c.data.name : "body";
269
+ if (hasBody)
270
+ lines.push(` String ${dataVar} = ${textBlock(c.request.body)};`);
271
+ lines.push(` given()`, ` .baseUri(${baseVar})`);
272
+ for (const a of authLine(c))
273
+ lines.push(` ${a}`);
274
+ for (const [k, v] of Object.entries(c.request.pathParams))
275
+ lines.push(` .pathParam("${quote(k)}", ${JSON.stringify(v)})`);
276
+ for (const [k, v] of Object.entries(c.request.query))
277
+ lines.push(` .queryParam("${quote(k)}", ${JSON.stringify(v)})`);
278
+ if (hasBody)
279
+ lines.push(` .contentType(ContentType.JSON)`, ` .body(${dataVar})`);
280
+ lines.push(` .when()`, ` .${c.method.toLowerCase()}("${quote(c.path)}")`, ` .then()`);
281
+ const checks = [
282
+ statusMatcher(c.expect.status),
283
+ ...c.kind === "happy" || c.kind === "error" ? bodyChecks(c.expect.fields, c.expect.body) : []
284
+ ];
285
+ checks.forEach((ch, i) => lines.push(` ${ch}${i === checks.length - 1 ? ";" : ""}`));
286
+ lines.push(" }");
287
+ return lines;
288
+ }
289
+ function apiClass(m, repo, pkg, cls, cases) {
290
+ const names = uniqueNames(cases.map((c) => camel(c.name)));
291
+ const body = cases.flatMap((c, i) => ["", ...apiMethod(c, names[i], "BASE_URL")]);
292
+ return [
293
+ ...header(pkg),
294
+ `/** API tests for story ${m.story.id} (change ${m.change}) on ${repo.component ?? repo.repo}. Base URL: ATTICUS_BASE_URL. */`,
295
+ `class ${cls} {`,
296
+ ` private static final String BASE_URL = env("ATTICUS_BASE_URL", "http://localhost:8080");`,
297
+ ...body,
298
+ "",
299
+ ...ENV,
300
+ "}",
301
+ ""
302
+ ].join("\n");
303
+ }
304
+ function contractMethod(c, name) {
305
+ const lines = [` @Test`, ` @DisplayName("${quote(c.name)}")`, ` void ${name}() {`];
306
+ for (const ch of c.checks)
307
+ lines.push(` // ${ch}`);
308
+ for (const g of c.gaps)
309
+ lines.push(` // Gap: ${g}`);
310
+ if (c.topic) {
311
+ lines.push(` Assumptions.abort("Connect the event test harness: ${quote(c.role === "consumer" ? `deliver a ${c.topic} message to ${c.consumer}` : `capture what ${c.provider} publishes to ${c.topic}`)}.");`, " }");
312
+ return lines;
313
+ }
314
+ const base = c.role === "consumer" ? "STUB_URL" : "BASE_URL";
315
+ const hasBody = c.request.example !== void 0;
316
+ if (hasBody)
317
+ lines.push(` String body = ${textBlock(c.request.example)};`);
318
+ lines.push(` given()`, ` .baseUri(${base})`);
319
+ if (c.role === "provider")
320
+ lines.push(` .auth().oauth2(env("ATTICUS_TOKEN"))`);
321
+ for (const m of (c.path ?? "").matchAll(/\{([^}]+)\}/g))
322
+ lines.push(` .pathParam("${quote(m[1])}", 1)`);
323
+ if (hasBody)
324
+ lines.push(` .contentType(ContentType.JSON)`, ` .body(body)`);
325
+ lines.push(` .when()`, ` .${(c.method ?? "get").toLowerCase()}("${quote(c.path ?? "/")}")`, ` .then()`);
326
+ const checks = [
327
+ c.response.status ? statusMatcher(c.response.status) : ".statusCode(lessThan(400))",
328
+ ...c.response.fields.map((f) => `.body("$", hasKey("${quote(f.split(/[.[]/)[0])}"))`)
329
+ ];
330
+ checks.forEach((ch, i) => lines.push(` ${ch}${i === checks.length - 1 ? ";" : ""}`));
331
+ lines.push(" }");
332
+ return lines;
333
+ }
334
+ function contractClass(m, pkg, cls, cases) {
335
+ const names = uniqueNames(cases.map((c) => camel(c.name)));
336
+ return [
337
+ ...header(pkg),
338
+ `/**`,
339
+ ` * Contract tests for story ${m.story.id} (change ${m.change}). Consumer tests run against a stub`,
340
+ ` * of the provider (atticus stub <provider>, at ATTICUS_STUB_URL); provider tests against the`,
341
+ ` * service at ATTICUS_BASE_URL.`,
342
+ ` */`,
343
+ `class ${cls} {`,
344
+ ` private static final String BASE_URL = env("ATTICUS_BASE_URL", "http://localhost:8080");`,
345
+ ` private static final String STUB_URL = env("ATTICUS_STUB_URL", "http://127.0.0.1:4010");`,
346
+ ...cases.flatMap((c, i) => ["", ...contractMethod(c, names[i])]),
347
+ "",
348
+ ...ENV,
349
+ "}",
350
+ ""
351
+ ].join("\n");
352
+ }
353
+ function dataClass(m, pkg, cls, cases) {
354
+ const names = uniqueNames(cases.map((c) => camel(c.name)));
355
+ const body = cases.flatMap((c, i) => {
356
+ const lines = [
357
+ "",
358
+ ` @Test`,
359
+ ` @DisplayName("${quote(c.name)}")`,
360
+ ` void ${names[i]}() throws Exception {`
361
+ ];
362
+ for (const ch of c.checks)
363
+ lines.push(` // ${ch}`);
364
+ if (c.kind === "migration") {
365
+ lines.push(` Assumptions.abort("Run ${quote(c.migration ?? "the migration")} on a copy of ${quote(c.store)} in the pipeline, then run this class.");`, " }");
366
+ return lines;
367
+ }
368
+ lines.push(` try (Connection db = connect()) {`);
369
+ if (c.kind === "table") {
370
+ lines.push(` assertTrue(exists(db, "${quote(c.table)}", null), "table ${quote(c.table)} exists");`);
371
+ for (const col of c.columns ?? [])
372
+ lines.push(` assertTrue(exists(db, "${quote(c.table)}", "${quote(col.name)}"), "column ${quote(col.name)} (${quote(col.type)}) exists");`);
373
+ } else {
374
+ lines.push(` assertTrue(exists(db, "${quote(c.table)}", "${quote(c.column)}"), "column ${quote(c.table)}.${quote(c.column)} exists");`);
375
+ if (c.notNull)
376
+ lines.push(` assertEquals("NO", nullable(db, "${quote(c.table)}", "${quote(c.column)}"), "${quote(c.column)} is not null");`);
377
+ }
378
+ lines.push(" }", " }");
379
+ return lines;
380
+ });
381
+ return [
382
+ `package ${pkg};`,
383
+ "",
384
+ "import static org.junit.jupiter.api.Assertions.*;",
385
+ "",
386
+ "import java.sql.Connection;",
387
+ "import java.sql.DriverManager;",
388
+ "import java.sql.PreparedStatement;",
389
+ "import java.sql.ResultSet;",
390
+ "import org.junit.jupiter.api.Assumptions;",
391
+ "import org.junit.jupiter.api.DisplayName;",
392
+ "import org.junit.jupiter.api.Test;",
393
+ "",
394
+ `/** Data tests for story ${m.story.id} (change ${m.change}): run against a migrated copy of the database at ATTICUS_DB_URL. */`,
395
+ `class ${cls} {`,
396
+ ...body,
397
+ "",
398
+ " private static Connection connect() throws Exception {",
399
+ ' String url = System.getenv("ATTICUS_DB_URL");',
400
+ ' Assumptions.assumeTrue(url != null && !url.isEmpty(), "Set ATTICUS_DB_URL to a migrated copy of the database.");',
401
+ ' return DriverManager.getConnection(url, System.getenv("ATTICUS_DB_USER"), System.getenv("ATTICUS_DB_PASSWORD"));',
402
+ " }",
403
+ "",
404
+ " private static boolean exists(Connection db, String table, String column) throws Exception {",
405
+ " String sql = column == null",
406
+ ' ? "select 1 from information_schema.tables where lower(table_name) = lower(?)"',
407
+ ' : "select 1 from information_schema.columns where lower(table_name) = lower(?) and lower(column_name) = lower(?)";',
408
+ " try (PreparedStatement q = db.prepareStatement(sql)) {",
409
+ " q.setString(1, table);",
410
+ " if (column != null) q.setString(2, column);",
411
+ " try (ResultSet r = q.executeQuery()) {",
412
+ " return r.next();",
413
+ " }",
414
+ " }",
415
+ " }",
416
+ "",
417
+ " private static String nullable(Connection db, String table, String column) throws Exception {",
418
+ ' String sql = "select is_nullable from information_schema.columns where lower(table_name) = lower(?) and lower(column_name) = lower(?)";',
419
+ " try (PreparedStatement q = db.prepareStatement(sql)) {",
420
+ " q.setString(1, table);",
421
+ " q.setString(2, column);",
422
+ " try (ResultSet r = q.executeQuery()) {",
423
+ " return r.next() ? r.getString(1) : null;",
424
+ " }",
425
+ " }",
426
+ " }",
427
+ "}",
428
+ ""
429
+ ].join("\n");
430
+ }
431
+ function eventClass(m, pkg, cls, cases) {
432
+ const names = uniqueNames(cases.map((c) => camel(c.name)));
433
+ return [
434
+ `package ${pkg};`,
435
+ "",
436
+ "import org.junit.jupiter.api.Assumptions;",
437
+ "import org.junit.jupiter.api.DisplayName;",
438
+ "import org.junit.jupiter.api.Test;",
439
+ "",
440
+ `/** Event tests for story ${m.story.id} (change ${m.change}). Connect them to the team's event test harness. */`,
441
+ `class ${cls} {`,
442
+ ...cases.flatMap((c, i) => [
443
+ "",
444
+ ` @Test`,
445
+ ` @DisplayName("${quote(c.name)}")`,
446
+ ` void ${names[i]}() {`,
447
+ ...c.checks.map((ch) => ` // ${ch}`),
448
+ ` Assumptions.abort("Connect the event test harness for ${quote(c.topic)}.");`,
449
+ " }"
450
+ ]),
451
+ "}",
452
+ ""
453
+ ].join("\n");
454
+ }
455
+ var junitGenerator = {
456
+ id: "junit5-restassured",
457
+ version: "1.0.0",
458
+ generic: false,
459
+ appliesTo: (repo) => repo.language === "java",
460
+ render(model, repo, ctx) {
461
+ const pkgName = `atticus.${model.change.replace(/[^a-z0-9]+/g, "_")}`;
462
+ const dir = `src/test/java/atticus/${model.change.replace(/[^a-z0-9]+/g, "_")}`;
463
+ const story = pascal(model.story.id);
464
+ const out = [];
465
+ const file2 = (suffix, content2, cases) => out.push({
466
+ path: `${dir}/${story}${suffix}.java`,
467
+ content: content2,
468
+ style: "slash",
469
+ source: ctx.source,
470
+ cases: cases.map((c) => c.id)
471
+ });
472
+ const api = repo.cases.filter((c) => c.type === "api");
473
+ const contracts = repo.cases.filter((c) => c.type === "contract");
474
+ const data = repo.cases.filter((c) => c.type === "data");
475
+ const events = repo.cases.filter((c) => c.type === "event");
476
+ if (api.length)
477
+ file2("ApiTest", apiClass(model, repo, pkgName, `${story}ApiTest`, api), api);
478
+ if (contracts.length)
479
+ file2("ContractTest", contractClass(model, pkgName, `${story}ContractTest`, contracts), contracts);
480
+ if (data.length)
481
+ file2("DataTest", dataClass(model, pkgName, `${story}DataTest`, data), data);
482
+ if (events.length)
483
+ file2("EventTest", eventClass(model, pkgName, `${story}EventTest`, events), events);
484
+ return out;
485
+ }
486
+ };
487
+
488
+ // packages/testgen/dist/generators/node.js
489
+ var CODE2 = /^[A-Z][A-Z0-9]*_[A-Z0-9_]+$/;
490
+ var json = (v, indent) => JSON.stringify(v, null, 2).split("\n").map((l, i) => i === 0 ? l : `${indent}${l}`).join("\n");
491
+ function statusCheck(status) {
492
+ if (/^\d{3}$/.test(status))
493
+ return `expect(res.status).toBe(${status});`;
494
+ const d = /^([1-5])XX$/i.exec(status)?.[1] ?? "4";
495
+ return `expect(Math.floor(res.status / 100)).toBe(${d});`;
496
+ }
497
+ var url = (base, path, params, query) => {
498
+ const p = path.replace(/\{([^}]+)\}/g, (_, k) => `\${encodeURIComponent(String(${JSON.stringify(params[k] ?? 1)}))}`);
499
+ const q = Object.entries(query).map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(String(v))}`).join("&");
500
+ return `\`\${${base}}${p}${q ? `?${q}` : ""}\``;
501
+ };
502
+ function headers(c) {
503
+ const h = [];
504
+ if (c.request.body !== void 0)
505
+ h.push(`"content-type": "application/json"`);
506
+ if (c.request.auth === "valid") {
507
+ if (c.request.authType === "basic")
508
+ h.push('authorization: `Basic ${Buffer.from(`${env("ATTICUS_USER")}:${env("ATTICUS_PASSWORD")}`).toString("base64")}`');
509
+ else if (c.request.authType === "apiKey" || c.request.authType === "api-key")
510
+ h.push(`[env("ATTICUS_API_KEY_HEADER", "x-api-key")]: env("ATTICUS_API_KEY")`);
511
+ else
512
+ h.push('authorization: `Bearer ${env("ATTICUS_TOKEN")}`');
513
+ } else if (c.request.auth === "no-scope") {
514
+ const v = `ATTICUS_TOKEN_WITHOUT_${c.request.scope.toUpperCase().replace(/[^A-Z0-9]+/g, "_")}`;
515
+ h.push(`authorization: \`Bearer \${env("${v}")}\``);
516
+ }
517
+ return h;
518
+ }
519
+ function apiTest(c) {
520
+ const lines = [];
521
+ const skip = c.request.auth === "no-scope";
522
+ const v = skip ? `ATTICUS_TOKEN_WITHOUT_${c.request.scope.toUpperCase().replace(/[^A-Z0-9]+/g, "_")}` : "";
523
+ lines.push(` it${skip ? `.skipIf(!process.env.${v})` : ""}("${quote(c.name)}", async () => {`);
524
+ if (c.kind === "error")
525
+ lines.push(` // ${c.why}`);
526
+ const dataVar = c.data && (c.kind === "happy" || c.kind === "error") ? c.data.name : "body";
527
+ if (c.request.body !== void 0)
528
+ lines.push(` const ${dataVar} = ${json(c.request.body, " ")};`);
529
+ const h = headers(c);
530
+ lines.push(` const res = await fetch(${url("BASE_URL", c.path, c.request.pathParams, c.request.query)}, {`, ` method: "${c.method}",`, ...h.length ? [` headers: { ${h.join(", ")} },`] : [], ...c.request.body !== void 0 ? [` body: JSON.stringify(${dataVar}),`] : [], ` });`, ` ${statusCheck(c.expect.status)}`);
531
+ if (c.kind === "happy" || c.kind === "error") {
532
+ const checks = [];
533
+ const b = c.expect.body;
534
+ if (b && typeof b === "object" && !Array.isArray(b)) {
535
+ for (const [k, val] of Object.entries(b))
536
+ if (typeof val === "string" && CODE2.test(val))
537
+ checks.push(` expect(json).toHaveProperty("${quote(k)}", "${quote(val)}");`);
538
+ }
539
+ for (const f of c.expect.fields)
540
+ if (!checks.some((x) => x.includes(`"${f}"`)))
541
+ checks.push(` expect(json).toHaveProperty("${quote(f)}");`);
542
+ if (checks.length)
543
+ lines.push(` const json = (await res.json()) as Record<string, unknown>;`, ...checks);
544
+ }
545
+ lines.push(" });");
546
+ return lines;
547
+ }
548
+ function contractTest(c) {
549
+ const lines = [` // ${c.checks.join("; ")}`, ...c.gaps.map((g) => ` // Gap: ${g}`)];
550
+ if (c.topic)
551
+ return [...lines, ` it.todo("${quote(c.name)}");`];
552
+ const base = c.role === "consumer" ? "STUB_URL" : "BASE_URL";
553
+ const params = Object.fromEntries([...(c.path ?? "").matchAll(/\{([^}]+)\}/g)].map((m) => [m[1], 1]));
554
+ lines.push(` it("${quote(c.name)}", async () => {`);
555
+ if (c.request.example !== void 0)
556
+ lines.push(` const body = ${json(c.request.example, " ")};`);
557
+ const h = [
558
+ ...c.request.example !== void 0 ? [`"content-type": "application/json"`] : [],
559
+ ...c.role === "provider" ? ['authorization: `Bearer ${env("ATTICUS_TOKEN")}`'] : []
560
+ ];
561
+ lines.push(` const res = await fetch(${url(base, c.path ?? "/", params, {})}, {`, ` method: "${c.method ?? "GET"}",`, ...h.length ? [` headers: { ${h.join(", ")} },`] : [], ...c.request.example !== void 0 ? [` body: JSON.stringify(body),`] : [], ` });`, c.response.status ? ` ${statusCheck(c.response.status)}` : ` expect(res.status).toBeLessThan(400);`);
562
+ if (c.response.fields.length)
563
+ lines.push(` const json = (await res.json()) as Record<string, unknown>;`, ...c.response.fields.map((f) => ` expect(json).toHaveProperty("${quote(f.split(/[.[]/)[0])}");`));
564
+ lines.push(" });");
565
+ return lines;
566
+ }
567
+ var todo = (c) => [
568
+ ` // ${c.checks.join("; ")}`,
569
+ ` it.todo("${quote(c.name)}");`
570
+ ];
571
+ function file(m, runner, what, body, bases) {
572
+ return [
573
+ runner === "vitest" ? `import { describe, expect, it } from "vitest";` : `import { describe, expect, it } from "@jest/globals";`,
574
+ "",
575
+ `const env = (name: string, fallback = "") => process.env[name] || fallback;`,
576
+ ...bases,
577
+ "",
578
+ `describe("${quote(`${what} for story ${m.story.id} (change ${m.change})`)}", () => {`,
579
+ ...body,
580
+ "});",
581
+ ""
582
+ ].join("\n");
583
+ }
584
+ function generator(runner) {
585
+ return {
586
+ id: `${runner}-fetch`,
587
+ version: "1.0.0",
588
+ generic: false,
589
+ appliesTo: (repo) => repo.language === "node" && (repo.runner ?? "vitest") === runner,
590
+ render(model, repo, ctx) {
591
+ const dir = "test/atticus";
592
+ const out = [];
593
+ const add = (kind, content2, cases) => out.push({
594
+ path: `${dir}/${model.story.id}.${kind}.test.ts`,
595
+ content: content2,
596
+ style: "slash",
597
+ source: ctx.source,
598
+ cases: cases.map((c) => c.id)
599
+ });
600
+ const BASE = `const BASE_URL = env("ATTICUS_BASE_URL", "http://localhost:3000");`;
601
+ const STUB = `const STUB_URL = env("ATTICUS_STUB_URL", "http://127.0.0.1:4010");`;
602
+ const api = repo.cases.filter((c) => c.type === "api");
603
+ const contracts = repo.cases.filter((c) => c.type === "contract");
604
+ const rest = repo.cases.filter((c) => c.type === "data" || c.type === "event");
605
+ const fixSkip = (lines) => runner === "jest" ? lines.map((l) => l.replace(/it\.skipIf\((![^)]+)\)\(/, (_, cond) => `(${cond} ? it.skip : it)(`)) : lines;
606
+ if (api.length)
607
+ add("api", file(model, runner, "API tests", fixSkip(api.flatMap((c, i) => [...i ? [""] : [], ...apiTest(c)])), [BASE]), api);
608
+ if (contracts.length)
609
+ add("contract", file(model, runner, "Contract tests", contracts.flatMap((c, i) => [...i ? [""] : [], ...contractTest(c)]), [BASE, STUB]), contracts);
610
+ if (rest.length)
611
+ add("data-and-events", file(model, runner, "Data and event tests", rest.flatMap((c, i) => [...i ? [""] : [], ...todo(c)]), []), rest);
612
+ return out;
613
+ }
614
+ };
615
+ }
616
+ var vitestGenerator = generator("vitest");
617
+ var jestGenerator = generator("jest");
618
+
619
+ // packages/testgen/dist/generators/index.js
620
+ var BUILT_IN_GENERATORS = [
621
+ gherkinGenerator,
622
+ jsonGenerator,
623
+ junitGenerator,
624
+ vitestGenerator,
625
+ jestGenerator
626
+ ];
627
+ function generatorsFor(repo, o = {}) {
628
+ const all = [...o.plugins ?? [], ...BUILT_IN_GENERATORS];
629
+ const generic = all.filter((g2) => g2.generic);
630
+ const concrete = all.filter((g2) => !g2.generic);
631
+ if (o.framework && ["gherkin", "json"].includes(o.framework))
632
+ return { generators: generic };
633
+ if (o.framework) {
634
+ const g2 = concrete.find((x) => x.id === o.framework);
635
+ return { generators: g2 ? [...generic, g2] : generic };
636
+ }
637
+ const g = concrete.find((x) => x.appliesTo(repo));
638
+ return g ? { generators: [...generic, g] } : {
639
+ generators: generic,
640
+ ...repo.repo ? {
641
+ note: `No test framework suits ${repo.repo} (its language isn't known): only Gherkin and JSON were written.`
642
+ } : {}
643
+ };
644
+ }
645
+
646
+ // packages/testgen/dist/header.js
647
+ import { createHash } from "node:crypto";
648
+ import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
649
+ import { dirname, join } from "node:path";
650
+ var sha256 = (s) => createHash("sha256").update(s, "utf8").digest("hex");
651
+ var TAG = "atticus-sha256";
652
+ var TEXT = (source, hash) => `Generated by Atticus from ${source}. Edit the source instead: Atticus regenerates this file and keeps hand edits only until --force. ${TAG}: ${hash}`;
653
+ var LINE_RE = new RegExp(`^(?:#|//) Generated by Atticus from (.+?)\\. .*${TAG}: ([0-9a-f]{64})$`);
654
+ var JSON_RE = new RegExp(`^\\{\\n "\\$generated": "Generated by Atticus from (.+?)\\. .*${TAG}: ([0-9a-f]{64})",\\n`);
655
+ function withHeader(f) {
656
+ const hash = sha256(f.content);
657
+ if (f.style === "json") {
658
+ if (!f.content.startsWith("{\n"))
659
+ throw new Error(`${f.path}: JSON content must be an object`);
660
+ return `{
661
+ "$generated": "${TEXT(f.source, hash)}",
662
+ ${f.content.slice(2)}`;
663
+ }
664
+ return `${f.style === "hash" ? "#" : "//"} ${TEXT(f.source, hash)}
665
+ ${f.content}`;
666
+ }
667
+ function readHeader(text) {
668
+ const t = text.replace(/\r\n/g, "\n");
669
+ const j = JSON_RE.exec(t);
670
+ if (j)
671
+ return { source: j[1], hash: j[2], body: `{
672
+ ${t.slice(j[0].length)}` };
673
+ const nl = t.indexOf("\n");
674
+ const first = nl < 0 ? t : t.slice(0, nl);
675
+ const m = LINE_RE.exec(first);
676
+ if (m)
677
+ return { source: m[1], hash: m[2], body: t.slice(nl + 1) };
678
+ return void 0;
679
+ }
680
+ async function writeGenerated(dir, files, o = {}) {
681
+ const out = [];
682
+ for (const f of files) {
683
+ if (f.path.startsWith("/") || f.path.split("/").includes("..") || /^[A-Za-z]:/.test(f.path))
684
+ throw new Error(`refusing to write outside the target folder: ${f.path}`);
685
+ const full = join(dir, ...f.path.split("/"));
686
+ const text = withHeader(f);
687
+ const current = await readFile(full, "utf8").then((t) => t.replace(/\r\n/g, "\n")).catch(() => void 0);
688
+ let action;
689
+ if (current === void 0)
690
+ action = "created";
691
+ else if (current === text)
692
+ action = "unchanged";
693
+ else {
694
+ const h = readHeader(current);
695
+ if (!h)
696
+ action = o.force ? "updated" : "kept-not-generated";
697
+ else if (sha256(h.body) !== h.hash)
698
+ action = o.force ? "updated" : "kept-edited";
699
+ else
700
+ action = "updated";
701
+ }
702
+ const write = !o.check && (action === "created" || action === "updated");
703
+ if (write) {
704
+ await mkdir(dirname(full), { recursive: true });
705
+ const tmp = `${full}.atticus-tmp`;
706
+ await writeFile(tmp, text, "utf8");
707
+ await rename(tmp, full);
708
+ }
709
+ out.push({ path: f.path, action, written: write });
710
+ }
711
+ return out;
712
+ }
713
+
714
+ // packages/testgen/dist/spec.js
715
+ import { existsSync, readFileSync } from "node:fs";
716
+ import { join as join2 } from "node:path";
717
+ var isObj = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
718
+ function candidates(ctx, hop, places) {
719
+ const out = [];
720
+ const calleeRepo = ctx.repoOf(hop.callee);
721
+ const place = calleeRepo ? places.get(calleeRepo) : void 0;
722
+ const contract = hop.contract.replace(/[`*]/g, "").trim();
723
+ if (contract && !/^(none|n\/a|-|tbd)$/i.test(contract)) {
724
+ if (place?.dir)
725
+ out.push({ file: join2(place.dir, contract), label: `${calleeRepo}:${contract}` });
726
+ out.push({ file: join2(ctx.root, contract), label: contract });
727
+ }
728
+ const ep = endpointFor(ctx, hop);
729
+ for (const e of ep?.evidence ?? []) {
730
+ if (e.source !== "spec" || !e.path)
731
+ continue;
732
+ const p = e.repo ? places.get(e.repo) : void 0;
733
+ if (e.repo && p?.dir)
734
+ out.push({ file: join2(p.dir, e.path), label: `${e.repo}:${e.path}` });
735
+ else if (!e.repo)
736
+ out.push({ file: join2(ctx.root, e.path), label: e.path });
737
+ }
738
+ return out;
739
+ }
740
+ function endpointFor(ctx, hop) {
741
+ if (!hop.method || !hop.path)
742
+ return void 0;
743
+ const p = normalizePath(hop.path);
744
+ return ctx.records.find((r) => r.kind === "Endpoint" && r.component === hop.callee && (r.method ?? "").toUpperCase() === hop.method && normalizePath(r.normalizedPath ?? r.pathOrTopic) === p);
745
+ }
746
+ var stripReadOnly = (schema) => {
747
+ if (!schema?.properties)
748
+ return schema;
749
+ return {
750
+ ...schema,
751
+ properties: Object.fromEntries(Object.entries(schema.properties).filter(([, s]) => !s.readOnly))
752
+ };
753
+ };
754
+ function securityOf(doc, op) {
755
+ const reqs = Array.isArray(op.security) ? op.security : Array.isArray(doc.security) ? doc.security : [];
756
+ const schemes = isObj(doc.components) && isObj(doc.components.securitySchemes) ? doc.components.securitySchemes : isObj(doc.securityDefinitions) ? doc.securityDefinitions : {};
757
+ const out = [];
758
+ for (const r of reqs) {
759
+ if (!isObj(r))
760
+ continue;
761
+ for (const [name, scopes] of Object.entries(r)) {
762
+ const s = isObj(schemes[name]) ? schemes[name] : {};
763
+ const type = s.type === "http" ? String(s.scheme ?? "http").toLowerCase() : String(s.type ?? "unknown");
764
+ out.push({ scheme: name, type, scopes: Array.isArray(scopes) ? scopes.map(String) : [] });
765
+ }
766
+ }
767
+ return out;
768
+ }
769
+ var pick = (content2) => {
770
+ if (!isObj(content2))
771
+ return void 0;
772
+ const k = Object.keys(content2).find((x) => x.toLowerCase() === "application/json") ?? Object.keys(content2).find((x) => /json/.test(x));
773
+ return k && isObj(content2[k]) ? content2[k] : void 0;
774
+ };
775
+ function findOperation(ctx, hop, places) {
776
+ const tried = [];
777
+ if (!hop.method || !hop.path)
778
+ return { tried };
779
+ const want = normalizePath(hop.path);
780
+ for (const c of candidates(ctx, hop, places)) {
781
+ if (!existsSync(c.file) || tried.includes(c.label))
782
+ continue;
783
+ tried.push(c.label);
784
+ let doc;
785
+ let stub;
786
+ try {
787
+ const text = readFileSync(c.file, "utf8");
788
+ doc = parseSpec(text);
789
+ stub = buildStub(text);
790
+ } catch {
791
+ continue;
792
+ }
793
+ const route = stub.routes.find((r) => {
794
+ if (r.method !== hop.method)
795
+ return false;
796
+ const p = normalizePath(r.path);
797
+ return p === want || stub.basePaths.some((b) => normalizePath(`${b}${r.path}`) === want);
798
+ });
799
+ if (!route)
800
+ continue;
801
+ const item = isObj(doc.paths) && isObj(doc.paths[route.path]) ? doc.paths[route.path] : {};
802
+ const op = isObj(item[route.method.toLowerCase()]) ? item[route.method.toLowerCase()] : {};
803
+ const media = pick(isObj(op.requestBody) ? op.requestBody.content : void 0);
804
+ const bodySchema = stripReadOnly(route.body?.schema);
805
+ const requestExample = media?.example !== void 0 ? media.example : route.body?.json && bodySchema ? generate(bodySchema) : void 0;
806
+ const ok = route.responses.find((r) => /^2/.test(r.status));
807
+ const okMedia = (() => {
808
+ const responses = isObj(op.responses) ? op.responses : {};
809
+ const r = ok ? responses[ok.status] : void 0;
810
+ return pick(isObj(r) ? r.content : void 0);
811
+ })();
812
+ const okSchema = okMedia && isObj(okMedia.schema) ? okMedia.schema : void 0;
813
+ return {
814
+ op: {
815
+ source: c.label,
816
+ route,
817
+ ...requestExample !== void 0 ? { requestExample } : {},
818
+ security: securityOf(doc, op),
819
+ requestFields: Object.keys(bodySchema?.properties ?? {}),
820
+ responseFields: Object.keys(okSchema?.properties ?? (isObj(ok?.body) ? ok.body : {}))
821
+ },
822
+ tried
823
+ };
824
+ }
825
+ return { tried };
826
+ }
827
+
828
+ // packages/testgen/dist/targets.js
829
+ import { existsSync as existsSync2, readFileSync as readFileSync2, statSync } from "node:fs";
830
+ import { join as join3 } from "node:path";
831
+ var isDir = (p) => {
832
+ try {
833
+ return statSync(p).isDirectory();
834
+ } catch {
835
+ return false;
836
+ }
837
+ };
838
+ function repoPlace(ctx, story, repo, changeRepos) {
839
+ for (const t of story.tasks.filter((x) => x.repo === repo)) {
840
+ const d = join3(changeWorktreesDir(ctx.root, ctx.change.id), ".tasks", t.id, repo);
841
+ if (isDir(d))
842
+ return { repo, dir: d, kind: "task-worktree", task: t.id };
843
+ }
844
+ const listed = changeRepos?.repos.find((r) => r.id === repo);
845
+ for (const d of [
846
+ ...listed ? [join3(ctx.root, ...listed.worktree.split("/"))] : [],
847
+ worktreePath(ctx.root, ctx.change.id, repo)
848
+ ])
849
+ if (isDir(d))
850
+ return { repo, dir: d, kind: "worktree" };
851
+ const base = baseClonePath(ctx.root, repo);
852
+ if (isDir(base))
853
+ return { repo, dir: base, kind: "base-clone" };
854
+ return { repo, kind: "none" };
855
+ }
856
+ var JAVA_TYPES = /\b(java|jvm|kotlin|spring|quarkus|micronaut|jakarta|scala)\b/i;
857
+ var NODE_TYPES = /node|spa|frontend|react|angular|vue|svelte|next|nest|express|typescript|javascript/i;
858
+ function languageOf(place, type) {
859
+ if (place.dir) {
860
+ const has = (p) => existsSync2(join3(place.dir, p));
861
+ if (has("pom.xml") || has("build.gradle") || has("build.gradle.kts") || has("src/main/java"))
862
+ return "java";
863
+ if (has("package.json"))
864
+ return "node";
865
+ }
866
+ if (type && JAVA_TYPES.test(type))
867
+ return "java";
868
+ if (type && NODE_TYPES.test(type))
869
+ return "node";
870
+ return void 0;
871
+ }
872
+ function nodeRunner(place) {
873
+ if (!place.dir)
874
+ return "vitest";
875
+ try {
876
+ const pkg = JSON.parse(readFileSync2(join3(place.dir, "package.json"), "utf8"));
877
+ const deps = { ...pkg.dependencies, ...pkg.devDependencies };
878
+ if (deps.vitest)
879
+ return "vitest";
880
+ if (deps.jest)
881
+ return "jest";
882
+ } catch {
883
+ }
884
+ return "vitest";
885
+ }
886
+
887
+ // packages/testgen/dist/model.js
888
+ var NEEDS = {
889
+ api: ["integration", "e2e", "regression", "security"],
890
+ contract: ["contract"],
891
+ data: ["data-migration", "integration"],
892
+ event: ["contract", "integration"]
893
+ };
894
+ var below = (min, integer) => integer ? min - 1 : min >= 1 ? min - 1 : min > 0 ? 0 : min - 1;
895
+ var typeOf = (s) => (Array.isArray(s.type) ? s.type.find((t) => t !== "null") : s.type) ?? (s.properties ? "object" : void 0);
896
+ function mutations(name, s) {
897
+ const out = [];
898
+ const t = typeOf(s);
899
+ if (s.enum?.length) {
900
+ let v = "INVALID";
901
+ while (s.enum.includes(v))
902
+ v += "-1";
903
+ out.push({ key: "enum", value: v, title: `refuses ${name} outside ${s.enum.join(", ")}` });
904
+ } else if (t === "integer" || t === "number")
905
+ out.push({ key: "type", value: "abc", title: `refuses ${name} that is not a number` });
906
+ else if (t === "string")
907
+ out.push({ key: "type", value: 12345, title: `refuses ${name} that is not text` });
908
+ else if (t === "boolean")
909
+ out.push({ key: "type", value: "yes", title: `refuses ${name} that is not true or false` });
910
+ if (typeof s.minimum === "number")
911
+ out.push({
912
+ key: "minimum",
913
+ value: below(s.minimum, t === "integer"),
914
+ title: `refuses ${name} below ${s.minimum}`
915
+ });
916
+ if (typeof s.exclusiveMinimum === "number")
917
+ out.push({
918
+ key: "exclusive-minimum",
919
+ value: s.exclusiveMinimum,
920
+ title: `refuses ${name} of ${s.exclusiveMinimum}`
921
+ });
922
+ if (typeof s.maximum === "number")
923
+ out.push({ key: "maximum", value: s.maximum + 1, title: `refuses ${name} above ${s.maximum}` });
924
+ if (typeof s.exclusiveMaximum === "number")
925
+ out.push({
926
+ key: "exclusive-maximum",
927
+ value: s.exclusiveMaximum,
928
+ title: `refuses ${name} of ${s.exclusiveMaximum}`
929
+ });
930
+ if (typeof s.minLength === "number" && s.minLength > 0)
931
+ out.push({
932
+ key: "min-length",
933
+ value: "x".repeat(s.minLength - 1),
934
+ title: `refuses ${name} shorter than ${s.minLength} characters`
935
+ });
936
+ if (typeof s.maxLength === "number")
937
+ out.push({
938
+ key: "max-length",
939
+ value: "x".repeat(s.maxLength + 1),
940
+ title: `refuses ${name} longer than ${s.maxLength} characters`
941
+ });
942
+ if (s.format && ["date", "date-time", "email", "uuid", "uri"].includes(s.format))
943
+ out.push({
944
+ key: "format",
945
+ value: `not-a-${s.format}`,
946
+ title: `refuses ${name} that is not a ${s.format}`
947
+ });
948
+ return out;
949
+ }
950
+ var CODE3 = /\b[A-Z][A-Z0-9]*_[A-Z0-9_]+\b/g;
951
+ function apiCases(hop, op, endpoint, acceptance) {
952
+ const notes = [];
953
+ const method = hop.method;
954
+ const path = op?.route.path ?? hop.path;
955
+ const base = `api.${slug(`${method} ${path}`)}`;
956
+ const common = {
957
+ component: hop.callee,
958
+ method,
959
+ path,
960
+ ...op?.route.operationId ? { operationId: op.route.operationId } : {},
961
+ .../^if-/.test(hop.id) ? { interfaceId: hop.id } : {},
962
+ ...endpoint ? { endpoint: endpoint.id } : {}
963
+ };
964
+ const pathParams = {};
965
+ const query = {};
966
+ for (const p of op?.route.params ?? []) {
967
+ const v = p.schema ? generate(p.schema) : "1";
968
+ if (p.in === "path")
969
+ pathParams[p.name] = v;
970
+ else if (p.in === "query" && p.required)
971
+ query[p.name] = v;
972
+ }
973
+ for (const m of path.matchAll(/\{([^}]+)\}/g))
974
+ if (!(m[1] in pathParams))
975
+ pathParams[m[1]] = "1";
976
+ const authType = op?.security[0]?.type ?? (endpoint?.auth && endpoint.auth !== "none" ? endpoint.auth : void 0);
977
+ const req = (o = {}) => ({
978
+ pathParams,
979
+ query,
980
+ ...op?.requestExample !== void 0 ? { body: op.requestExample } : {},
981
+ auth: authType ? "valid" : "none",
982
+ ...authType ? { authType } : {},
983
+ ...o
984
+ });
985
+ const cases = [];
986
+ const data = op?.requestExample !== void 0 ? { id: `data.${slug(`${method} ${path}`)}`, name: "" } : void 0;
987
+ const statuses = new Set((op?.route.responses ?? []).map((r) => r.status));
988
+ const bad = statuses.has("400") ? "400" : statuses.has("422") && !op?.route.responses.find((r) => r.status === "422")?.body ? "422" : "4XX";
989
+ const ok = op?.route.responses.find((r) => /^2/.test(r.status));
990
+ const okStatus = op ? String(op.route.status) : method === "POST" ? "201" : "200";
991
+ cases.push({
992
+ type: "api",
993
+ id: `${base}.happy`,
994
+ kind: "happy",
995
+ title: `${method} ${path} accepts a valid request`,
996
+ name: "",
997
+ ...common,
998
+ request: req(),
999
+ expect: {
1000
+ status: okStatus,
1001
+ ...ok?.body !== void 0 && ok.example !== "none" ? { body: ok.body } : {},
1002
+ fields: op?.responseFields ?? []
1003
+ },
1004
+ why: op ? `The spec (${op.source}) answers ${okStatus} for a valid request.` : "The design calls this operation; no spec was found.",
1005
+ ...data ? { data } : {}
1006
+ });
1007
+ if (!op)
1008
+ notes.push(`No spec has ${method} ${hop.path} for ${hop.callee}: only the happy path was generated.`);
1009
+ const schema = op?.route.body?.schema;
1010
+ const example = op?.requestExample;
1011
+ if (schema?.properties && example && typeof example === "object" && !Array.isArray(example)) {
1012
+ for (const f of schema.required ?? []) {
1013
+ const body = { ...example };
1014
+ delete body[f];
1015
+ cases.push({
1016
+ type: "api",
1017
+ id: `${base}.missing-${slug(f)}`,
1018
+ kind: "negative",
1019
+ title: `${method} ${path} refuses a request without ${f}`,
1020
+ name: "",
1021
+ ...common,
1022
+ request: req({ body }),
1023
+ expect: { status: bad, fields: [] },
1024
+ why: `${f} is required by the spec.`
1025
+ });
1026
+ }
1027
+ for (const [f, s] of Object.entries(schema.properties)) {
1028
+ if (s.readOnly)
1029
+ continue;
1030
+ for (const m of mutations(f, s))
1031
+ cases.push({
1032
+ type: "api",
1033
+ id: `${base}.${slug(f)}-${m.key}`,
1034
+ kind: "negative",
1035
+ title: `${method} ${path} ${m.title}`,
1036
+ name: "",
1037
+ ...common,
1038
+ request: req({ body: { ...example, [f]: m.value } }),
1039
+ expect: { status: bad, fields: [] },
1040
+ why: `The spec constrains ${f} (${m.key.replace(/-/g, " ")}).`
1041
+ });
1042
+ }
1043
+ }
1044
+ for (const p of op?.route.params ?? []) {
1045
+ if (p.in !== "path" || !p.schema)
1046
+ continue;
1047
+ for (const m of mutations(p.name, p.schema))
1048
+ cases.push({
1049
+ type: "api",
1050
+ id: `${base}.${slug(p.name)}-${m.key}`,
1051
+ kind: "negative",
1052
+ title: `${method} ${path} ${m.title.replace("refuses ", "refuses a path ")}`,
1053
+ name: "",
1054
+ ...common,
1055
+ request: req({ pathParams: { ...pathParams, [p.name]: m.value } }),
1056
+ expect: { status: bad, fields: [] },
1057
+ why: `The spec constrains the path parameter ${p.name} (${m.key.replace(/-/g, " ")}).`
1058
+ });
1059
+ }
1060
+ for (const r of op?.route.responses ?? []) {
1061
+ if (!/^[45]\d\d$/.test(r.status) || ["400", "401", "403"].includes(r.status))
1062
+ continue;
1063
+ const said = `${r.description ?? ""} ${JSON.stringify(r.body ?? "")}`;
1064
+ const codes = [...new Set(said.match(CODE3) ?? [])];
1065
+ const scenario = acceptance.find((a) => codes.some((c) => a.then.includes(c)) || new RegExp(`\\b${r.status}\\b`).test(a.then));
1066
+ cases.push({
1067
+ type: "api",
1068
+ id: `${base}.status-${r.status}`,
1069
+ kind: "error",
1070
+ title: `${method} ${path} answers ${r.status}${r.description ? `: ${r.description}` : ""}`,
1071
+ name: "",
1072
+ ...common,
1073
+ request: req(),
1074
+ expect: { status: r.status, ...r.body !== void 0 ? { body: r.body } : {}, fields: [] },
1075
+ why: `The spec documents ${r.status}${r.description ? `: ${r.description}` : ""}. The request needs data in that state${scenario ? ` (${scenario.trace?.name || `criterion ${scenario.criterion}`})` : ""}.`,
1076
+ ...scenario ? { scenario: scenario.id } : {},
1077
+ ...data ? { data } : {}
1078
+ });
1079
+ }
1080
+ if (authType) {
1081
+ cases.push({
1082
+ type: "api",
1083
+ id: `${base}.no-credentials`,
1084
+ kind: "auth",
1085
+ title: `${method} ${path} refuses a request without credentials`,
1086
+ name: "",
1087
+ ...common,
1088
+ request: req({ auth: "none" }),
1089
+ expect: { status: "401", fields: [] },
1090
+ why: `The operation needs ${authType} credentials.`
1091
+ });
1092
+ for (const s of op?.security ?? [])
1093
+ for (const scope of s.scopes)
1094
+ cases.push({
1095
+ type: "api",
1096
+ id: `${base}.no-scope-${slug(scope)}`,
1097
+ kind: "auth",
1098
+ title: `${method} ${path} refuses a token without scope ${scope}`,
1099
+ name: "",
1100
+ ...common,
1101
+ request: req({ auth: "no-scope", scope }),
1102
+ expect: { status: "403", fields: [] },
1103
+ why: `The spec asks for scope ${scope}.`
1104
+ });
1105
+ }
1106
+ return { cases, notes };
1107
+ }
1108
+ function mappedFields(text) {
1109
+ const rows = text.split("\n").filter((l) => l.trim().startsWith("|"));
1110
+ if (rows.length < 2)
1111
+ return [];
1112
+ const cells2 = (l) => l.trim().replace(/^\||\|$/g, "").split("|").map((c) => c.replace(/[`*]/g, "").trim());
1113
+ const head = cells2(rows[0]).map((h) => h.toLowerCase());
1114
+ const ti = head.findIndex((h) => /^target( field)?$/.test(h));
1115
+ if (ti < 0)
1116
+ return [];
1117
+ return [
1118
+ ...new Set(rows.slice(1).map(cells2).filter((c) => !c.every((x) => /^:?-+:?$/.test(x) || x === "")).map((c) => c[ti] ?? "").filter((f) => /^[A-Za-z_][\w.[\]-]*$/.test(f)))
1119
+ ];
1120
+ }
1121
+ var topicOf = (op) => /^(publish|subscribe|send|receive)\s+(\S+)/i.exec(op.trim());
1122
+ function contractCases(ctx, hop, op, tried) {
1123
+ if (!/^planned-(new|modify)$/.test(hop.status))
1124
+ return [];
1125
+ const mapping = ctx.mapping(hop);
1126
+ const notes = ctx.notes(hop).map((n) => n.text).join("\n");
1127
+ const fields = mappedFields(mapping);
1128
+ const errors = [];
1129
+ for (const m of `${notes}
1130
+ ${mapping}`.matchAll(/HTTP\s+(\d{3})/g)) {
1131
+ const around = `${notes}
1132
+ ${mapping}`;
1133
+ const code = (around.match(CODE3) ?? [])[0];
1134
+ if (!errors.some((e) => e.status === m[1]))
1135
+ errors.push({ status: m[1], ...code ? { code } : {} });
1136
+ }
1137
+ const t = topicOf(hop.operation);
1138
+ const base = `contract.${slug(t ? `${t[1]} ${t[2]}` : `${hop.method} ${hop.path}`)}`;
1139
+ const common = {
1140
+ status: hop.status,
1141
+ .../^if-/.test(hop.id) ? { interfaceId: hop.id } : {}
1142
+ };
1143
+ if (t) {
1144
+ const publish = /^(publish|send)$/i.test(t[1]);
1145
+ const publisher = publish ? hop.caller : hop.callee;
1146
+ const subscriber = publish ? hop.callee : hop.caller;
1147
+ const shared2 = {
1148
+ ...common,
1149
+ consumer: subscriber,
1150
+ provider: publisher,
1151
+ topic: t[2],
1152
+ request: { fields },
1153
+ response: { fields: [] },
1154
+ errors: [],
1155
+ gaps: []
1156
+ };
1157
+ return [
1158
+ {
1159
+ type: "contract",
1160
+ id: `${base}.subscriber`,
1161
+ title: `${subscriber} handles a ${t[2]} message from ${publisher}`,
1162
+ name: "",
1163
+ role: "consumer",
1164
+ ...shared2,
1165
+ checks: [
1166
+ `${subscriber} accepts a message on ${t[2]}`,
1167
+ ...fields.length ? [`it reads ${fields.join(", ")}`] : []
1168
+ ]
1169
+ },
1170
+ {
1171
+ type: "contract",
1172
+ id: `${base}.publisher`,
1173
+ title: `${publisher} publishes ${t[2]} as ${subscriber} expects`,
1174
+ name: "",
1175
+ role: "provider",
1176
+ ...shared2,
1177
+ checks: [
1178
+ `${publisher} publishes to ${t[2]}`,
1179
+ ...fields.length ? [`the message carries ${fields.join(", ")}`] : []
1180
+ ]
1181
+ }
1182
+ ];
1183
+ }
1184
+ if (!hop.method || !hop.path)
1185
+ return [];
1186
+ const route = hopRoutes(ctx.files, hop).map((id) => ctx.get("GatewayRoute", id)).find((r) => r !== void 0);
1187
+ const consumerPath = route ? `${route.basepath.replace(/\/+$/, "")}${hop.path}` : hop.path;
1188
+ const gaps = [];
1189
+ const req = [];
1190
+ const res = [];
1191
+ const read = hop.method === "GET" || hop.method === "DELETE" || hop.method === "HEAD";
1192
+ for (const f of fields) {
1193
+ const top = f.split(/[.[]/)[0];
1194
+ if (op?.requestFields.includes(top))
1195
+ req.push(f);
1196
+ else if (op?.responseFields.includes(top))
1197
+ res.push(f);
1198
+ else {
1199
+ (read ? res : req).push(f);
1200
+ if (op)
1201
+ gaps.push(`The consumer maps ${f}, which ${hop.callee}'s spec (${op.source}) doesn't define.`);
1202
+ }
1203
+ }
1204
+ if (!op)
1205
+ gaps.push(tried.length ? `${tried.join(", ")} ${tried.length === 1 ? "has" : "have"} no ${hop.method} ${hop.path}: the provider's side can't be checked until the spec has it.` : `No spec for ${hop.callee} was found (interfaces.md contract: ${hop.contract.trim() || "none"}): the provider's side can't be checked.`);
1206
+ const statuses = new Set((op?.route.responses ?? []).map((r) => r.status));
1207
+ for (const e of errors)
1208
+ if (op && !statuses.has(e.status))
1209
+ gaps.push(`The design says ${hop.method} ${hop.path} answers ${e.status}, which the spec doesn't document.`);
1210
+ const okStatus = op ? String(op.route.status) : void 0;
1211
+ const required = op?.route.body?.schema?.required ?? [];
1212
+ const shared = {
1213
+ ...common,
1214
+ consumer: hop.caller,
1215
+ provider: hop.callee,
1216
+ method: hop.method,
1217
+ path: hop.path,
1218
+ consumerPath,
1219
+ request: {
1220
+ fields: req,
1221
+ ...op?.requestExample !== void 0 ? { example: op.requestExample } : {}
1222
+ },
1223
+ response: {
1224
+ ...okStatus ? { status: okStatus } : {},
1225
+ fields: res,
1226
+ ...op?.route.responses.find((r) => /^2/.test(r.status))?.body !== void 0 ? { example: op.route.responses.find((r) => /^2/.test(r.status)).body } : {}
1227
+ },
1228
+ errors,
1229
+ gaps,
1230
+ ...op ? { spec: op.source } : {}
1231
+ };
1232
+ return [
1233
+ {
1234
+ type: "contract",
1235
+ id: `${base}.consumer`,
1236
+ title: `${hop.caller} calls ${hop.method} ${consumerPath} as ${hop.callee}'s contract says`,
1237
+ name: "",
1238
+ role: "consumer",
1239
+ ...shared,
1240
+ checks: [
1241
+ `${hop.caller} sends ${hop.method} ${consumerPath}${req.length ? ` with ${req.join(", ")}` : ""}`,
1242
+ ...res.length ? [`it reads ${res.join(", ")} from the answer`] : [],
1243
+ ...errors.map((e) => `it handles ${e.status}${e.code ? ` ${e.code}` : ""}`)
1244
+ ]
1245
+ },
1246
+ {
1247
+ type: "contract",
1248
+ id: `${base}.provider`,
1249
+ title: `${hop.callee} serves ${hop.method} ${hop.path} as ${hop.caller} expects`,
1250
+ name: "",
1251
+ role: "provider",
1252
+ ...shared,
1253
+ checks: [
1254
+ `${hop.callee} serves ${hop.method} ${hop.path}`,
1255
+ ...required.length ? [`it requires ${required.join(", ")}`] : [],
1256
+ ...okStatus ? [`it answers ${okStatus}${res.length ? ` with ${res.join(", ")}` : ""}`] : [],
1257
+ ...errors.map((e) => `it answers ${e.status}${e.code ? ` with ${e.code}` : ""}`)
1258
+ ]
1259
+ }
1260
+ ];
1261
+ }
1262
+ function dataCases(d) {
1263
+ const common = { store: d.store, table: d.table, tableId: d.tableId };
1264
+ const cases = [];
1265
+ const gaps = [];
1266
+ const base = `data.${slug(d.table)}`;
1267
+ if (d.newTable) {
1268
+ cases.push({
1269
+ type: "data",
1270
+ id: `${base}.table`,
1271
+ title: `table ${d.table} exists with its columns`,
1272
+ name: "",
1273
+ kind: "table",
1274
+ ...common,
1275
+ columns: d.newColumns,
1276
+ checks: [
1277
+ `${d.store} has table ${d.table}`,
1278
+ ...d.newColumns.map((c) => `column ${c.name} is ${c.type}`)
1279
+ ]
1280
+ });
1281
+ } else {
1282
+ for (const c of d.newColumns) {
1283
+ const m = new RegExp(`add column\\s+${c.name}\\s+[\\w(),]+([^,;]*)`, "i").exec(d.change);
1284
+ const rest = m?.[1] ?? "";
1285
+ const notNull = /\bnot null\b/i.test(rest);
1286
+ const def = /\bdefault\s+('[^']*'|\S+)/i.exec(rest)?.[1];
1287
+ cases.push({
1288
+ type: "data",
1289
+ id: `${base}.${slug(c.name)}`,
1290
+ title: `column ${d.table}.${c.name} exists as ${c.type}`,
1291
+ name: "",
1292
+ kind: "column",
1293
+ ...common,
1294
+ column: c.name,
1295
+ columnType: c.type,
1296
+ ...notNull ? { notNull } : {},
1297
+ ...def ? { default: def } : {},
1298
+ checks: [
1299
+ `${d.table}.${c.name} is ${c.type}`,
1300
+ ...notNull ? [`${d.table}.${c.name} is not null`] : [],
1301
+ ...def ? [`existing rows get ${def}`] : []
1302
+ ]
1303
+ });
1304
+ }
1305
+ }
1306
+ if (d.migration)
1307
+ cases.push({
1308
+ type: "data",
1309
+ id: `${base}.migration`,
1310
+ title: `migration ${d.migration} applies on a copy of ${d.store}`,
1311
+ name: "",
1312
+ kind: "migration",
1313
+ ...common,
1314
+ migration: d.migration,
1315
+ checks: [
1316
+ `${d.migration} applies cleanly on a copy of ${d.store}`,
1317
+ `afterwards ${d.newTable ? `table ${d.table} exists` : `${d.newColumns.map((c) => `${d.table}.${c.name}`).join(", ")} exist`}`
1318
+ ]
1319
+ });
1320
+ else if (d.newTable || d.newColumns.length)
1321
+ gaps.push(`data.md names no migration for ${d.table}: the migration test can't be generated.`);
1322
+ return { cases, gaps };
1323
+ }
1324
+ function eventCases(hop, fields, firstWhen) {
1325
+ const t = topicOf(hop.operation);
1326
+ if (!t)
1327
+ return [];
1328
+ const publish = /^(publish|send)$/i.test(t[1]);
1329
+ const publisher = publish ? hop.caller : hop.callee;
1330
+ const subscriber = publish ? hop.callee : hop.caller;
1331
+ const base = `event.${slug(t[2])}`;
1332
+ const common = {
1333
+ topic: t[2],
1334
+ publisher,
1335
+ subscriber,
1336
+ fields,
1337
+ .../^if-/.test(hop.id) ? { interfaceId: hop.id } : {}
1338
+ };
1339
+ return [
1340
+ {
1341
+ type: "event",
1342
+ id: `${base}.publish`,
1343
+ title: `${publisher} publishes ${t[2]}${firstWhen ? ` when ${firstWhen}` : ""}`,
1344
+ name: "",
1345
+ role: "publisher",
1346
+ ...common,
1347
+ checks: [
1348
+ `a message is published to ${t[2]}${firstWhen ? ` when ${firstWhen}` : ""}`,
1349
+ ...fields.length ? [`it carries ${fields.join(", ")}`] : []
1350
+ ]
1351
+ },
1352
+ {
1353
+ type: "event",
1354
+ id: `${base}.consume`,
1355
+ title: `${subscriber} handles ${t[2]}`,
1356
+ name: "",
1357
+ role: "subscriber",
1358
+ ...common,
1359
+ checks: [
1360
+ `${subscriber} processes a message from ${t[2]} once, even if it is delivered twice`
1361
+ ]
1362
+ }
1363
+ ];
1364
+ }
1365
+ function buildTestModel(ctx, story, changeRepos) {
1366
+ const required = ctx.tests.required;
1367
+ const rowKinds = ctx.tests.matrix ?? ctx.tests.required;
1368
+ const row = ctx.tests.row ? `${ctx.tests.row.changeType}/${ctx.tests.row.risk}` : void 0;
1369
+ const askedBy = /* @__PURE__ */ new Map();
1370
+ for (const a of ctx.tests.added ?? [])
1371
+ for (const k of a.kinds)
1372
+ if (!rowKinds.includes(k) && !askedBy.has(k))
1373
+ askedBy.set(k, { factor: a.factor, why: a.why });
1374
+ const kinds = [
1375
+ { type: "acceptance", generated: true, why: "Every acceptance criterion gets a scenario." }
1376
+ ];
1377
+ for (const [type, needs] of Object.entries(NEEDS)) {
1378
+ const hit = needs.filter((n) => rowKinds.includes(n));
1379
+ const added = needs.filter((n) => askedBy.has(n));
1380
+ const fromRow = hit.length ? `test-strategy.yaml${row ? ` (${row})` : ""} asks for ${hit.join(", ")}.` : "";
1381
+ const fromRisk = added.map((n) => {
1382
+ const a = askedBy.get(n);
1383
+ return `The change's ${a.factor} risk asks for ${n} tests ${a.why}.`;
1384
+ }).join(" ");
1385
+ kinds.push({
1386
+ type,
1387
+ generated: hit.length + added.length > 0,
1388
+ why: [fromRow, fromRisk].filter(Boolean).join(" ") || `test-strategy.yaml${row ? ` (${row})` : ""} asks for ${required.join(", ") || "nothing"}, not ${needs.join(" or ")}.`
1389
+ });
1390
+ }
1391
+ const on = (t) => kinds.find((k) => k.type === t).generated;
1392
+ const notes = [];
1393
+ const gaps = [];
1394
+ if (ctx.tests.note)
1395
+ notes.push(ctx.tests.note);
1396
+ if (story.linkedBy === "inferred")
1397
+ notes.push(`${story.id} has no links section: its interfaces and data changes were found by what it mentions.`);
1398
+ const accTags = ["e2e", "regression", "accessibility"].filter((k) => required.includes(k));
1399
+ const acceptance = story.criteria.map((c) => ({
1400
+ type: "acceptance",
1401
+ id: `acceptance.c${c.n}`,
1402
+ title: c.trace?.name || `Criterion ${c.n}`,
1403
+ name: "",
1404
+ criterion: c.n,
1405
+ given: c.given,
1406
+ when: c.when,
1407
+ then: c.then,
1408
+ text: c.text,
1409
+ ...c.trace ? { trace: c.trace } : {},
1410
+ tags: accTags
1411
+ }));
1412
+ if (!story.criteria.length)
1413
+ gaps.push(`${story.id} has no acceptance criteria: no scenario was generated.`);
1414
+ const places = /* @__PURE__ */ new Map();
1415
+ const allRepos = new Set(story.repos);
1416
+ for (const h of story.hops)
1417
+ for (const c of [h.caller, h.callee]) {
1418
+ const r = ctx.repoOf(c);
1419
+ if (r)
1420
+ allRepos.add(r);
1421
+ }
1422
+ const ownerRepo = (d) => {
1423
+ const s = ctx.get("DataStore", d.store);
1424
+ return s?.ownerComponent ? ctx.repoOf(s.ownerComponent) : void 0;
1425
+ };
1426
+ for (const d of story.data) {
1427
+ const r = ownerRepo(d);
1428
+ if (r)
1429
+ allRepos.add(r);
1430
+ }
1431
+ for (const r of allRepos)
1432
+ places.set(r, repoPlace(ctx, story, r, changeRepos));
1433
+ const repoCases = new Map([...allRepos].map((r) => [r, []]));
1434
+ const add = (repo, cs) => {
1435
+ if (!repo || !cs.length)
1436
+ return false;
1437
+ repoCases.set(repo, [...repoCases.get(repo) ?? [], ...cs]);
1438
+ return true;
1439
+ };
1440
+ const elsewhere = [];
1441
+ for (const h of story.hops) {
1442
+ const calleeRepo = ctx.repoOf(h.callee);
1443
+ const isRest = !!h.method && !!h.path && !topicOf(h.operation);
1444
+ const found = isRest ? findOperation(ctx, h, places) : { tried: [] };
1445
+ const op = found.op;
1446
+ if (isRest && on("api")) {
1447
+ if (calleeRepo) {
1448
+ const a = apiCases(h, op, endpointFor(ctx, h), acceptance);
1449
+ add(calleeRepo, a.cases);
1450
+ notes.push(...a.notes);
1451
+ } else
1452
+ notes.push(`${h.callee} has no repo here, so its ${h.method} ${h.path} gets no API tests; ${h.caller}'s contract tests cover the call.`);
1453
+ }
1454
+ if (on("contract")) {
1455
+ for (const c of contractCases(ctx, h, op, found.tried)) {
1456
+ const repo = ctx.repoOf(c.role === "consumer" ? c.consumer : c.provider);
1457
+ if (!add(repo, [c]))
1458
+ elsewhere.push(c);
1459
+ if (c.role === "provider")
1460
+ gaps.push(...c.gaps);
1461
+ }
1462
+ }
1463
+ if (on("event") && topicOf(h.operation)) {
1464
+ const evs = eventCases(h, mappedFields(ctx.mapping(h)), story.criteria[0]?.when);
1465
+ for (const e of evs)
1466
+ add(ctx.repoOf(e.role === "publisher" ? e.publisher : e.subscriber), [e]);
1467
+ }
1468
+ }
1469
+ if (on("data"))
1470
+ for (const d of story.data) {
1471
+ const dc = dataCases(d);
1472
+ const repo = ownerRepo(d);
1473
+ if (!add(repo, dc.cases) && dc.cases.length)
1474
+ notes.push(`No repo owns ${d.store}, so the data tests for ${d.table} were not written.`);
1475
+ gaps.push(...dc.gaps);
1476
+ }
1477
+ const primary = story.repos.find((r) => places.has(r)) ?? [...repoCases.keys()].find((r) => repoCases.get(r).length);
1478
+ if (primary)
1479
+ repoCases.set(primary, [...acceptance, ...repoCases.get(primary) ?? []]);
1480
+ const repos = [];
1481
+ for (const [repo, cases] of repoCases) {
1482
+ if (!cases.length)
1483
+ continue;
1484
+ const place = places.get(repo) ?? { repo, kind: "none" };
1485
+ const manifest = ctx.lifecycle.orgPack.profiles.flatMap((p) => p.repos?.repos ?? []).find((r) => r.id === repo);
1486
+ const language = languageOf(place, manifest?.type);
1487
+ const component = ctx.componentOfRepo(repo);
1488
+ repos.push({
1489
+ repo,
1490
+ ...component ? { component } : {},
1491
+ ...language ? { language } : {},
1492
+ ...language === "node" ? { runner: nodeRunner(place) } : {},
1493
+ place,
1494
+ primary: repo === primary,
1495
+ cases
1496
+ });
1497
+ }
1498
+ if (!primary && acceptance.length)
1499
+ repos.push({ repo: "", place: { repo: "", kind: "none" }, primary: true, cases: acceptance });
1500
+ return {
1501
+ change: ctx.change.id,
1502
+ story: {
1503
+ id: story.id,
1504
+ path: story.path,
1505
+ title: story.title,
1506
+ ...story.parsed.who ? {
1507
+ asA: story.parsed.who.asA,
1508
+ iWant: story.parsed.who.iWant,
1509
+ soThat: story.parsed.who.soThat
1510
+ } : {},
1511
+ ...story.tracesTo ? { tracesTo: story.tracesTo } : {}
1512
+ },
1513
+ strategy: {
1514
+ required,
1515
+ ...row ? { row } : {},
1516
+ ...ctx.tests.note ? { note: ctx.tests.note } : {}
1517
+ },
1518
+ kinds,
1519
+ repos: repos.sort((a, b) => Number(b.primary) - Number(a.primary) || a.repo.localeCompare(b.repo)),
1520
+ elsewhere,
1521
+ linkedBy: story.linkedBy,
1522
+ notes: [...new Set(notes)],
1523
+ gaps: [...new Set(gaps)]
1524
+ };
1525
+ }
1526
+ var allCases = (m) => [
1527
+ ...m.repos.flatMap((r) => r.cases),
1528
+ ...m.elsewhere
1529
+ ];
1530
+
1531
+ // packages/testgen/dist/names.js
1532
+ var import_yaml = __toESM(require_dist(), 1);
1533
+ var TEST_WRITER = "test-writer";
1534
+ function testWriter(orgPack) {
1535
+ const own = orgPack?.agents.find((a) => a.id === TEST_WRITER);
1536
+ if (own)
1537
+ return own;
1538
+ return AgentDefinition.parse({
1539
+ schemaVersion: 1,
1540
+ id: TEST_WRITER,
1541
+ role: "qa",
1542
+ description: "Names the scenarios, test cases and test data of tests Atticus generates from a story, in plain words.",
1543
+ inputs: ["stories", "specs", "catalog-slice"],
1544
+ outputArtifact: "code",
1545
+ tokenBudget: orgPack?.runtime.budgets.defaultTokens ?? 15e4,
1546
+ allowedRuntimes: ["hand-off", "model", "headless-claude-code", "mock", "replay"]
1547
+ });
1548
+ }
1549
+ var INSTRUCTIONS = `# Test writer
1550
+
1551
+ Atticus has generated the tests for one story; their structure, steps, requests and checks are decided. You only name things, in plain, short English:
1552
+
1553
+ - scenarios: a title for each Gherkin scenario, at most 80 characters, saying what is checked;
1554
+ - cases: a test name for each test case, at most 80 characters, starting with a verb;
1555
+ - data: a name for each piece of test data, two to five words, saying what it is (it becomes a variable name).
1556
+
1557
+ Use only what the material says. Don't add cases or change what they check. Write YAML and nothing else:
1558
+
1559
+ scenarios:
1560
+ - id: <scenario id, exactly as given>
1561
+ name: <title>
1562
+ cases:
1563
+ - id: <case id, exactly as given>
1564
+ name: <test name>
1565
+ data:
1566
+ - id: <data id, exactly as given>
1567
+ name: <data name>`;
1568
+ function namingMaterial(m) {
1569
+ const cases = allCases(m);
1570
+ const data = /* @__PURE__ */ new Map();
1571
+ for (const c of cases)
1572
+ if (c.type === "api" && c.data && !data.has(c.data.id))
1573
+ data.set(c.data.id, `request body for ${c.method} ${c.path}: ${JSON.stringify(c.request.body)}`);
1574
+ return toCanonicalYaml({
1575
+ story: { id: m.story.id, title: m.story.title },
1576
+ scenarios: cases.filter((c) => c.type === "acceptance").map((c) => ({
1577
+ id: c.id,
1578
+ given: c.given,
1579
+ when: c.when,
1580
+ then: c.then,
1581
+ ...c.trace?.name ? { specScenario: c.trace.name } : {}
1582
+ })),
1583
+ cases: cases.filter((c) => c.type !== "acceptance").map((c) => ({ id: c.id, what: c.title })),
1584
+ data: [...data].map(([id, what]) => ({ id, what }))
1585
+ }, ["story", "scenarios", "cases", "data", "id", "title", "given", "when", "then", "what"]).trimEnd();
1586
+ }
1587
+ var Named = external_exports.object({ id: external_exports.string().min(1), name: external_exports.string().min(1) });
1588
+ var Answer = external_exports.object({
1589
+ scenarios: external_exports.array(external_exports.unknown()).default([]),
1590
+ cases: external_exports.array(external_exports.unknown()).default([]),
1591
+ data: external_exports.array(external_exports.unknown()).default([])
1592
+ });
1593
+ function applyNames(m, output) {
1594
+ const discarded = [];
1595
+ let data;
1596
+ try {
1597
+ data = (0, import_yaml.parse)(output.replace(/^```(?:ya?ml)?\s*\n|\n```\s*$/g, ""));
1598
+ } catch (e) {
1599
+ return [{ reason: `the answer isn't YAML: ${e.message.split("\n")[0]}` }];
1600
+ }
1601
+ const a = Answer.safeParse(data);
1602
+ if (!a.success)
1603
+ return [{ reason: "the answer has no scenarios, cases or data lists" }];
1604
+ const cases = allCases(m);
1605
+ const byId = new Map(cases.map((c) => [c.id, c]));
1606
+ const apply = (list, ok, set) => {
1607
+ for (const raw of list) {
1608
+ const n = Named.safeParse(raw);
1609
+ if (!n.success) {
1610
+ discarded.push({ reason: "an entry without an id and a name" });
1611
+ continue;
1612
+ }
1613
+ const name = oneLine(n.data.name, 100);
1614
+ if (!ok(n.data.id)) {
1615
+ discarded.push({ id: n.data.id, reason: "no such id in the generated tests" });
1616
+ continue;
1617
+ }
1618
+ if (!name) {
1619
+ discarded.push({ id: n.data.id, reason: "an empty name" });
1620
+ continue;
1621
+ }
1622
+ set(n.data.id, name);
1623
+ }
1624
+ };
1625
+ apply(a.data.scenarios, (id) => byId.get(id)?.type === "acceptance", (id, name) => {
1626
+ for (const c of cases)
1627
+ if (c.id === id)
1628
+ c.name = name;
1629
+ });
1630
+ apply(a.data.cases, (id) => !!byId.get(id) && byId.get(id).type !== "acceptance", (id, name) => {
1631
+ for (const c of cases)
1632
+ if (c.id === id)
1633
+ c.name = name;
1634
+ });
1635
+ const dataIds = new Set(cases.flatMap((c) => c.type === "api" && c.data ? [c.data.id] : []));
1636
+ apply(a.data.data, (id) => dataIds.has(id), (id, name) => {
1637
+ for (const c of cases)
1638
+ if (c.type === "api" && c.data?.id === id)
1639
+ c.data = { id, name: camel(name, 40) };
1640
+ });
1641
+ return discarded;
1642
+ }
1643
+ function fallbackNames(m) {
1644
+ for (const c of allCases(m)) {
1645
+ if (!c.name)
1646
+ c.name = c.title;
1647
+ if (c.type === "api" && c.data && !c.data.name)
1648
+ c.data = { id: c.data.id, name: camel(`${c.method} ${c.path} request`, 40) };
1649
+ }
1650
+ }
1651
+ async function nameTests(m, o) {
1652
+ if (!o.runtime) {
1653
+ fallbackNames(m);
1654
+ return { by: "fallback", discarded: [] };
1655
+ }
1656
+ const agent = testWriter(o.orgPack);
1657
+ const levels = o.orgPack?.pack?.classification ?? [
1658
+ "public",
1659
+ "internal",
1660
+ "confidential",
1661
+ "restricted"
1662
+ ];
1663
+ const sources = [
1664
+ {
1665
+ id: "instructions",
1666
+ kind: "instruction",
1667
+ origin: "atticus/testgen",
1668
+ text: INSTRUCTIONS,
1669
+ classification: "public",
1670
+ trusted: true
1671
+ },
1672
+ {
1673
+ id: "tests",
1674
+ kind: "artifact",
1675
+ origin: `atticus tests generate ${m.story.id} (deterministic)`,
1676
+ text: namingMaterial(m),
1677
+ classification: "internal",
1678
+ priority: 100
1679
+ }
1680
+ ];
1681
+ const pack = buildContextPack({
1682
+ change: m.change,
1683
+ agent: agent.id,
1684
+ budget: o.orgPack?.runtime.budgets.perAgent[agent.id] ?? agent.tokenBudget,
1685
+ sources,
1686
+ levels,
1687
+ maxClassification: levels.includes("internal") ? "internal" : levels[0],
1688
+ now: o.now
1689
+ });
1690
+ let run;
1691
+ if (o.resume)
1692
+ run = await o.runtime.result(o.resume);
1693
+ else {
1694
+ const prepared = await o.runtime.prepare({
1695
+ change: m.change,
1696
+ agent,
1697
+ pack,
1698
+ outputPath: `tests/${m.story.id}/names.yaml`
1699
+ });
1700
+ run = await o.runtime.run(prepared.runId);
1701
+ }
1702
+ if (run.state === "waiting" || run.state === "prepared") {
1703
+ fallbackNames(m);
1704
+ return {
1705
+ by: "fallback",
1706
+ discarded: [],
1707
+ waiting: run.runId,
1708
+ promptSha256: pack.manifest.promptSha256
1709
+ };
1710
+ }
1711
+ if (run.output === void 0 || run.state === "failed" || run.state === "cancelled") {
1712
+ fallbackNames(m);
1713
+ return {
1714
+ by: "fallback",
1715
+ discarded: [
1716
+ { reason: `the test writer ${run.state}: ${run.errors.join("; ") || "no output"}` }
1717
+ ],
1718
+ run: toRunSummary(run),
1719
+ promptSha256: pack.manifest.promptSha256
1720
+ };
1721
+ }
1722
+ const discarded = applyNames(m, run.output);
1723
+ fallbackNames(m);
1724
+ return { by: "ai", discarded, run: toRunSummary(run), promptSha256: pack.manifest.promptSha256 };
1725
+ }
1726
+
1727
+ // packages/testgen/dist/story.js
1728
+ import { readFile as readFile2 } from "node:fs/promises";
1729
+ import { join as join4 } from "node:path";
1730
+ var GWT = /^given\s+(.+?),?\s+when\s+(.+?),?\s+then\s+(.+?)\.?(?:\s*\(([^()]+?):(\d+)\))?\.?\s*$/i;
1731
+ var content = (s) => new Set(s.toLowerCase().replace(/[^a-z0-9_ ]+/g, " ").split(/\s+/).filter((w) => w.length > 2 && !STOP.has(w)));
1732
+ var STOP = /* @__PURE__ */ new Set([
1733
+ "the",
1734
+ "and",
1735
+ "with",
1736
+ "that",
1737
+ "has",
1738
+ "its",
1739
+ "for",
1740
+ "asks",
1741
+ "when",
1742
+ "then",
1743
+ "given",
1744
+ "error"
1745
+ ]);
1746
+ function overlap(a, b) {
1747
+ const x = content(a);
1748
+ const y = content(b);
1749
+ if (!x.size || !y.size)
1750
+ return 0;
1751
+ let n = 0;
1752
+ for (const w of x)
1753
+ if (y.has(w))
1754
+ n++;
1755
+ return n / (x.size + y.size - n);
1756
+ }
1757
+ function traceCriterion(text, ref, scenarios) {
1758
+ if (ref) {
1759
+ const hit = scenarios.find((x) => x.file === ref.file && x.s.line === ref.line);
1760
+ return { file: ref.file, line: ref.line, name: hit?.s.name ?? "" };
1761
+ }
1762
+ let best;
1763
+ for (const x of scenarios) {
1764
+ const score = overlap(text, `${x.s.when.join(" ")} ${x.s.then.join(" ")}`);
1765
+ if (score >= 0.5 && (!best || score > best.score))
1766
+ best = { ...x, score };
1767
+ }
1768
+ return best ? { file: best.file, line: best.s.line, name: best.s.name } : void 0;
1769
+ }
1770
+ function splitCriterion(text) {
1771
+ text = text.replace(/\s*\(also accepts tasks? [^()]*\)\s*$/i, "");
1772
+ const m = GWT.exec(text.trim());
1773
+ if (!m)
1774
+ return { given: "", when: "", then: text.trim().replace(/\.$/, "") };
1775
+ return {
1776
+ given: m[1].trim(),
1777
+ when: m[2].trim(),
1778
+ then: m[3].trim(),
1779
+ ...m[4] ? { ref: { file: m[4].trim(), line: Number(m[5]) } } : {}
1780
+ };
1781
+ }
1782
+ var LINK_IF = /^Interface\s+(\S+?):\s*(\S+)\s*→\s*(\S+)\s+(.+?)\s*(?:\(([^)]*)\))?\s*$/;
1783
+ var LINK_REC = /^Record\s+([a-z0-9][a-z0-9._-]*?)[:.]?(?:\s|$)/;
1784
+ function links(doc) {
1785
+ const s = doc.sections.find((x) => x.level === 2 && /^links$/i.test(x.heading));
1786
+ const interfaces = [];
1787
+ const records = [];
1788
+ if (!s)
1789
+ return { interfaces, records };
1790
+ for (const l of sectionLines(doc, s)) {
1791
+ const b = /^\s*[-*]\s+(.*)$/.exec(l.text)?.[1]?.trim();
1792
+ if (!b)
1793
+ continue;
1794
+ const i = LINK_IF.exec(b);
1795
+ if (i) {
1796
+ const line = /interfaces\.md:(\d+)/.exec(i[5] ?? "")?.[1];
1797
+ interfaces.push({
1798
+ id: i[1].replace(/[()]/g, ""),
1799
+ caller: i[2],
1800
+ callee: i[3],
1801
+ operation: i[4].trim(),
1802
+ ...line ? { line: Number(line) } : {}
1803
+ });
1804
+ continue;
1805
+ }
1806
+ const r = LINK_REC.exec(b);
1807
+ if (r)
1808
+ records.push(r[1]);
1809
+ }
1810
+ return { interfaces, records };
1811
+ }
1812
+ var opKey = (op) => {
1813
+ const m = /^([A-Za-z]+)\s+(\S+)/.exec(op.trim());
1814
+ return m ? `${m[1].toUpperCase()} ${/^\//.test(m[2]) ? normalizePath(m[2]) : m[2]}` : op.trim();
1815
+ };
1816
+ var TASK = /^\s*[-*]\s+\[[ xX]?\]\s+(\S+)\s+(.*)$/;
1817
+ function storyTasks(ctx, story) {
1818
+ const doc = docOf(ctx.files, "tasks.md");
1819
+ if (!doc)
1820
+ return [];
1821
+ const out = [];
1822
+ let repo = "";
1823
+ doc.lines.forEach((l, i) => {
1824
+ const h = /^##\s+(.+?)\s*$/.exec(l);
1825
+ if (h) {
1826
+ repo = h[1].replace(/[`*]/g, "").trim().split(/\s+/)[0].toLowerCase();
1827
+ return;
1828
+ }
1829
+ const m = TASK.exec(l);
1830
+ if (!m || !repo || !RecordId.safeParse(m[1].toLowerCase()).success)
1831
+ return;
1832
+ const stories = [...m[2].matchAll(/\bstor(?:y|ies)\s+([a-z0-9][a-z0-9._-]*)/gi)].map((s) => s[1].toLowerCase().replace(/[.,)]+$/, ""));
1833
+ if (stories.includes(story))
1834
+ out.push({ id: m[1].toLowerCase(), repo, text: m[2], line: i + 1 });
1835
+ });
1836
+ return out;
1837
+ }
1838
+ async function loadStory(ctx, storyId) {
1839
+ if (!RecordId.safeParse(storyId).success)
1840
+ throw new TestgenError("invalid", `"${storyId}" is not a story id.`);
1841
+ const path = `stories/${storyId}.md`;
1842
+ let text;
1843
+ try {
1844
+ text = await readFile2(join4(ctx.dir, "stories", `${storyId}.md`), "utf8");
1845
+ } catch {
1846
+ throw new TestgenError("not-found", `${ctx.change.id} has no story ${storyId} (no ${path}). Plan the stories first: atticus plan stories ${ctx.change.id}`);
1847
+ }
1848
+ const parsed = parseStory(path, text);
1849
+ const scenarios = ctx.requirements.flatMap((r) => r.scenarios.map((s) => ({ file: r.file, s })));
1850
+ const criteriaLines = (() => {
1851
+ const s = parsed.doc.sections.find((x) => x.level === 2 && /acceptance criteria/i.test(x.heading));
1852
+ return s ? sectionLines(parsed.doc, s).filter((l2) => /^\s*[-*]\s+/.test(l2.text)) : [];
1853
+ })();
1854
+ const criteria = parsed.criteria.map((c, i) => {
1855
+ const p = splitCriterion(c);
1856
+ const trace = traceCriterion(c, p.ref, scenarios);
1857
+ return {
1858
+ n: i + 1,
1859
+ text: c,
1860
+ given: p.given,
1861
+ when: p.when,
1862
+ then: p.then,
1863
+ ...trace ? { trace } : {},
1864
+ line: criteriaLines[i]?.n ?? 1
1865
+ };
1866
+ });
1867
+ const l = links(parsed.doc);
1868
+ let hops = [];
1869
+ let linkedBy = "links";
1870
+ if (l.interfaces.length) {
1871
+ for (const li of l.interfaces) {
1872
+ const h = (li.line ? ctx.hops.find((x) => x.line === li.line) : void 0) ?? (li.id !== "new" ? ctx.hops.find((x) => x.id === li.id) : void 0) ?? ctx.hops.find((x) => x.caller === li.caller && x.callee === li.callee && opKey(x.operation) === opKey(li.operation));
1873
+ if (h && !hops.includes(h))
1874
+ hops.push(h);
1875
+ }
1876
+ } else {
1877
+ linkedBy = "inferred";
1878
+ const said = [
1879
+ text,
1880
+ ...ctx.requirements.find((r) => parsed.traces?.includes(`${r.file}:${r.line}`))?.text ?? []
1881
+ ].join("\n");
1882
+ hops = ctx.hops.filter((h) => {
1883
+ const op = h.path ?? h.operation.replace(/^\S+\s+/, "");
1884
+ return op && said.includes(op);
1885
+ });
1886
+ }
1887
+ const tasks = storyTasks(ctx, storyId);
1888
+ let data = ctx.data.filter((d) => l.records.includes(d.tableId));
1889
+ if (!data.length && !l.records.length) {
1890
+ const taskText = tasks.map((t) => t.text).join("\n");
1891
+ data = ctx.data.filter((d) => d.migration && taskText.includes(d.migration) || taskText.includes(`table ${d.table}`));
1892
+ if (data.length)
1893
+ linkedBy = "inferred";
1894
+ }
1895
+ const repos = [
1896
+ .../* @__PURE__ */ new Set([...parsed.repos.map((r) => r.toLowerCase()), ...tasks.map((t) => t.repo)])
1897
+ ].filter((r) => RecordId.safeParse(r).success);
1898
+ return {
1899
+ id: storyId,
1900
+ path,
1901
+ parsed,
1902
+ title: parsed.title ?? storyId,
1903
+ criteria,
1904
+ hops,
1905
+ data,
1906
+ repos,
1907
+ tasks,
1908
+ linkedBy,
1909
+ ...parsed.traces ? { tracesTo: /^(\S+:\d+)/.exec(parsed.traces)?.[1] ?? parsed.traces } : {}
1910
+ };
1911
+ }
1912
+
1913
+ // packages/testgen/dist/generate.js
1914
+ var ManifestFile = external_exports.strictObject({
1915
+ schemaVersion: external_exports.literal(1),
1916
+ change: RecordId,
1917
+ story: RecordId,
1918
+ strategy: external_exports.array(external_exports.string()),
1919
+ names: external_exports.enum(["ai", "fallback"]),
1920
+ files: external_exports.array(external_exports.strictObject({
1921
+ path: external_exports.string().min(1),
1922
+ repo: external_exports.string(),
1923
+ generator: external_exports.string().min(1),
1924
+ cases: external_exports.array(external_exports.string())
1925
+ })),
1926
+ cases: external_exports.array(external_exports.strictObject({
1927
+ id: external_exports.string().min(1),
1928
+ type: external_exports.enum(["acceptance", "api", "contract", "data", "event"]),
1929
+ repo: external_exports.string(),
1930
+ name: external_exports.string(),
1931
+ criterion: external_exports.number().int().positive().optional(),
1932
+ scenario: external_exports.string().optional(),
1933
+ endpoint: external_exports.string().optional(),
1934
+ interface: external_exports.string().optional(),
1935
+ method: external_exports.string().optional(),
1936
+ path: external_exports.string().optional(),
1937
+ table: external_exports.string().optional(),
1938
+ topic: external_exports.string().optional(),
1939
+ role: external_exports.string().optional()
1940
+ }))
1941
+ });
1942
+ var posix = (p) => p.split(sep).join("/");
1943
+ function manifestCase(c, repo) {
1944
+ const base = { id: c.id, type: c.type, repo, name: c.name };
1945
+ switch (c.type) {
1946
+ case "acceptance":
1947
+ return {
1948
+ ...base,
1949
+ criterion: c.criterion,
1950
+ ...c.trace ? { scenario: `${c.trace.file}:${c.trace.line}` } : {}
1951
+ };
1952
+ case "api":
1953
+ return {
1954
+ ...base,
1955
+ ...c.scenario ? { criterion: Number(c.scenario.replace(/^acceptance\.c/, "")) } : {},
1956
+ method: c.method,
1957
+ path: c.path,
1958
+ ...c.endpoint ? { endpoint: c.endpoint } : {},
1959
+ ...c.interfaceId ? { interface: c.interfaceId } : {}
1960
+ };
1961
+ case "contract":
1962
+ return {
1963
+ ...base,
1964
+ role: c.role,
1965
+ ...c.method ? { method: c.method } : {},
1966
+ ...c.path ? { path: c.path } : {},
1967
+ ...c.topic ? { topic: c.topic } : {},
1968
+ ...c.interfaceId ? { interface: c.interfaceId } : {}
1969
+ };
1970
+ case "data":
1971
+ return { ...base, table: c.tableId };
1972
+ case "event":
1973
+ return {
1974
+ ...base,
1975
+ topic: c.topic,
1976
+ role: c.role,
1977
+ ...c.interfaceId ? { interface: c.interfaceId } : {}
1978
+ };
1979
+ }
1980
+ }
1981
+ async function generateTests(root, changeId, storyId, o = {}) {
1982
+ const known = [...o.generators ?? [], ...BUILT_IN_GENERATORS].map((g) => g.id);
1983
+ if (o.framework && !known.includes(o.framework))
1984
+ throw new TestgenError("invalid", `There is no test framework ${o.framework}. Frameworks: ${known.join(", ")}.`);
1985
+ const ctx = await loadPlanContext(root, changeId, { requireApprovedDesign: false }).catch((e) => {
1986
+ throw new TestgenError("not-found", e.message);
1987
+ });
1988
+ if (ctx.change.state === "archived")
1989
+ throw new TestgenError("refused", `${ctx.change.id} is archived; its tests can't be generated again.`);
1990
+ const story = await loadStory(ctx, storyId);
1991
+ const changeRepos = await readChangeRepos(root, changeId);
1992
+ const model = buildTestModel(ctx, story, changeRepos);
1993
+ const names = await nameTests(model, {
1994
+ ...o.runtime ? { runtime: o.runtime } : {},
1995
+ orgPack: ctx.lifecycle.orgPack,
1996
+ now: o.now ?? /* @__PURE__ */ new Date(),
1997
+ ...o.resume ? { resume: o.resume } : {}
1998
+ });
1999
+ const changeRel = posix(relative(root, ctx.dir));
2000
+ const source = `${changeRel}/${story.path}`;
2001
+ const files = [];
2002
+ const notes = [...model.notes];
2003
+ for (const repo of model.repos) {
2004
+ const inWorktree = repo.place.kind === "task-worktree" || repo.place.kind === "worktree";
2005
+ const dir = inWorktree ? repo.place.dir : join5(ctx.dir, "tests", story.id, ...repo.repo ? [repo.repo] : []);
2006
+ const where = inWorktree ? repo.place.kind : "change-folder";
2007
+ const chosen = generatorsFor(repo, {
2008
+ ...o.framework ? { framework: o.framework } : {},
2009
+ ...o.generators ? { plugins: o.generators } : {}
2010
+ });
2011
+ if (chosen.note)
2012
+ notes.push(chosen.note);
2013
+ for (const g of chosen.generators) {
2014
+ const rendered = g.render(model, repo, { source });
2015
+ const results = await writeGenerated(dir, rendered, {
2016
+ ...o.force ? { force: true } : {},
2017
+ ...o.check ? { check: true } : {}
2018
+ });
2019
+ results.forEach((r, i) => files.push({
2020
+ repo: repo.repo,
2021
+ where,
2022
+ generator: g.id,
2023
+ path: posix(relative(root, join5(dir, ...r.path.split("/")))),
2024
+ action: r.action,
2025
+ written: r.written,
2026
+ cases: rendered[i].cases ?? []
2027
+ }));
2028
+ }
2029
+ }
2030
+ for (const f of files.filter((x) => x.action === "kept-edited"))
2031
+ notes.push(`${f.path} was edited by a person, so it was kept; use --force to replace it.`);
2032
+ for (const f of files.filter((x) => x.action === "kept-not-generated"))
2033
+ notes.push(`${f.path} wasn't written by Atticus, so it was kept; use --force to replace it.`);
2034
+ const manifestRel = `${changeRel}/tests/${story.id}/manifest.yaml`;
2035
+ if (!o.check) {
2036
+ const m = ManifestFile.parse({
2037
+ schemaVersion: 1,
2038
+ change: model.change,
2039
+ story: story.id,
2040
+ strategy: model.strategy.required,
2041
+ names: names.by,
2042
+ files: files.map((f) => ({
2043
+ path: f.path,
2044
+ repo: f.repo,
2045
+ generator: f.generator,
2046
+ cases: f.cases
2047
+ })),
2048
+ cases: model.repos.flatMap((r) => r.cases.map((c) => manifestCase(c, r.repo)))
2049
+ });
2050
+ await writeFileAtomic(join5(root, ...manifestRel.split("/")), toCanonicalYaml(m, ["schemaVersion", "change", "story"]));
2051
+ }
2052
+ return {
2053
+ change: model.change,
2054
+ story: story.id,
2055
+ model,
2056
+ names,
2057
+ files,
2058
+ manifest: manifestRel,
2059
+ notes: [...new Set(notes)],
2060
+ gaps: model.gaps
2061
+ };
2062
+ }
2063
+ async function readManifests(changeDir) {
2064
+ const manifests = [];
2065
+ const problems = [];
2066
+ let dirs;
2067
+ try {
2068
+ dirs = (await readdir(join5(changeDir, "tests"), { withFileTypes: true })).filter((d) => d.isDirectory()).map((d) => d.name).sort();
2069
+ } catch {
2070
+ return { manifests, problems };
2071
+ }
2072
+ for (const d of dirs) {
2073
+ const text = await readFile3(join5(changeDir, "tests", d, "manifest.yaml"), "utf8").catch(() => void 0);
2074
+ if (text === void 0)
2075
+ continue;
2076
+ const r = ManifestFile.safeParse((0, import_yaml2.parse)(text));
2077
+ if (r.success)
2078
+ manifests.push(r.data);
2079
+ else
2080
+ problems.push(`tests/${d}/manifest.yaml: ${r.error.issues[0]?.message ?? "invalid"}`);
2081
+ }
2082
+ return { manifests, problems };
2083
+ }
2084
+
2085
+ // packages/testgen/dist/trace.js
2086
+ var import_yaml3 = __toESM(require_dist(), 1);
2087
+ import { existsSync as existsSync3 } from "node:fs";
2088
+ import { readdir as readdir2, readFile as readFile4 } from "node:fs/promises";
2089
+ import { fileURLToPath } from "node:url";
2090
+ import { join as join7, relative as relative2, sep as sep2 } from "node:path";
2091
+
2092
+ // packages/testgen/dist/rounds.js
2093
+ import { join as join6 } from "node:path";
2094
+ var sig = (fs) => fs.map((f) => `${fingerprint(f)}|${f.state}|${f.severity}`).sort().join("\n");
2095
+ async function writeGateRound(root, change, gate, fresh, now) {
2096
+ const rounds = await roundsOf(root, change, gate);
2097
+ const last = rounds.at(-1);
2098
+ const prev = last ? (await readRound(root, change, gate, last)).findings.map((s) => s.finding) : [];
2099
+ const byPrint = new Map(prev.map((f) => [fingerprint(f), f]));
2100
+ const taken = /* @__PURE__ */ new Set();
2101
+ const next = [];
2102
+ for (const f of fresh) {
2103
+ const before = byPrint.get(fingerprint(f));
2104
+ let id = f.id;
2105
+ for (let i = 2; taken.has(id); i++)
2106
+ id = `${f.id}-${i}`;
2107
+ taken.add(id);
2108
+ const merged = Finding.safeParse({
2109
+ ...f,
2110
+ id,
2111
+ createdAt: before?.createdAt ?? f.createdAt,
2112
+ ...before?.resolution ? { state: before.state, resolution: before.resolution } : {}
2113
+ });
2114
+ next.push(merged.success ? merged.data : { ...f, id });
2115
+ }
2116
+ if (sig(next) === sig(prev) && (last !== void 0 || next.length === 0)) {
2117
+ const findings2 = last ? (await readRound(root, change, gate, last)).findings : [];
2118
+ return { gate, ...last ? { round: last } : {}, unchanged: true, findings: findings2 };
2119
+ }
2120
+ const round = (last ?? 0) + 1;
2121
+ const findings = [];
2122
+ for (const f of next.sort((a, b) => a.id.localeCompare(b.id))) {
2123
+ const g = Finding.parse({ ...f, round, gate, change });
2124
+ await writeFinding(root, g);
2125
+ findings.push({ finding: g, path: `review/${gate}/${round}/${g.id}.yaml` });
2126
+ }
2127
+ await writeFileAtomic(join6(changeFolder(root, change), "review", gate, String(round), "meta", "round.yaml"), toCanonicalYaml({
2128
+ schemaVersion: 1,
2129
+ change,
2130
+ gate,
2131
+ round,
2132
+ createdAt: now,
2133
+ findings: findings.map((f) => ({
2134
+ id: f.finding.id,
2135
+ rule: f.finding.rule,
2136
+ severity: f.finding.severity
2137
+ }))
2138
+ }, ["schemaVersion", "change", "gate", "round", "createdAt"]));
2139
+ return { gate, round, unchanged: false, findings };
2140
+ }
2141
+
2142
+ // packages/testgen/dist/trace.js
2143
+ var __atticus_import_meta_url = __atticusUrl("packages/testgen/dist/trace.js");
2144
+ var TRACE_GATE = "trace";
2145
+ var TEST_FILE = /(^|\/)(src\/test|tests?|__tests__|spec|specs|e2e)\/|\.(test|spec)\.[a-z]+$|Tests?\.(java|kt|cs|py|go|scala)$|_test\.(go|py)$|(^|\/)test_[^/]+\.py$|\.feature$/;
2146
+ var SKIP = /* @__PURE__ */ new Set([
2147
+ "node_modules",
2148
+ ".git",
2149
+ "target",
2150
+ "build",
2151
+ "dist",
2152
+ ".gradle",
2153
+ ".idea",
2154
+ ".atticus"
2155
+ ]);
2156
+ var posix2 = (p) => p.split(sep2).join("/");
2157
+ var escape = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
2158
+ async function walk(dir, rel = "", out = []) {
2159
+ let entries;
2160
+ try {
2161
+ entries = await readdir2(join7(dir, rel), { withFileTypes: true });
2162
+ } catch {
2163
+ return out;
2164
+ }
2165
+ for (const e of entries.sort((a, b) => a.name.localeCompare(b.name))) {
2166
+ const p = rel ? `${rel}/${e.name}` : e.name;
2167
+ if (e.isDirectory()) {
2168
+ if (!SKIP.has(e.name))
2169
+ await walk(dir, p, out);
2170
+ } else if (e.isFile() && TEST_FILE.test(p))
2171
+ out.push(p);
2172
+ }
2173
+ return out;
2174
+ }
2175
+ function pathPattern(path) {
2176
+ const segs = normalizePath(path).split("/").filter(Boolean);
2177
+ const body = segs.map((s) => s === "{}" ? "[^/\"'`\\s?]+" : escape(s)).join("/");
2178
+ return new RegExp(`/${body}(?=["'\`?\\s)]|$)`, "m");
2179
+ }
2180
+ function requirementsOf(ctx) {
2181
+ const out = [];
2182
+ for (const r of ctx.requirements) {
2183
+ if (!r.scenarios.length)
2184
+ out.push({ file: r.file, line: r.line, requirement: r.name, key: `${r.file}:${r.line}` });
2185
+ for (const s of r.scenarios)
2186
+ out.push({
2187
+ file: r.file,
2188
+ line: s.line,
2189
+ requirement: r.name,
2190
+ scenario: s.name,
2191
+ key: `${r.file}:${s.line}`
2192
+ });
2193
+ }
2194
+ return out;
2195
+ }
2196
+ async function storyIds(dir) {
2197
+ return (await readdir2(join7(dir, "stories")).catch(() => [])).filter((f) => f.endsWith(".md")).map((f) => f.replace(/\.md$/, "")).sort();
2198
+ }
2199
+ async function codeOf(ctx, story, dirOf, git, problems, notStarted) {
2200
+ const out = /* @__PURE__ */ new Set();
2201
+ for (const t of story.tasks) {
2202
+ const text = await readFile4(join7(ctx.dir, "tasks", t.id, "task-run.yaml"), "utf8").catch(() => void 0);
2203
+ if (text) {
2204
+ const run = (0, import_yaml3.parse)(text);
2205
+ for (const f of Array.isArray(run.changed) ? run.changed : [])
2206
+ out.add(`${t.repo}:${String(f)}`);
2207
+ }
2208
+ const dir = dirOf(t.repo);
2209
+ if (!git || !dir || !existsSync3(join7(dir, ".git")))
2210
+ continue;
2211
+ try {
2212
+ const r = await git([
2213
+ "diff",
2214
+ "--name-only",
2215
+ `${changeBranch(ctx.change.id)}...${taskBranch(ctx.change.id, t.id)}`
2216
+ ], dir);
2217
+ for (const f of r.stdout.split("\n").map((x) => x.trim()).filter(Boolean))
2218
+ out.add(`${t.repo}:${f}`);
2219
+ } catch (e) {
2220
+ const branch = `refs/heads/${taskBranch(ctx.change.id, t.id)}`;
2221
+ const exists = await git(["rev-parse", "--verify", "--quiet", branch], dir).then(() => true, () => false);
2222
+ if (exists)
2223
+ problems.push(`${t.repo}: no diff for task ${t.id} (${e.message.split("\n")[0]})`);
2224
+ else if (!text)
2225
+ notStarted.add(`${t.repo} ${t.id}`);
2226
+ }
2227
+ }
2228
+ return [...out].sort();
2229
+ }
2230
+ var isTaskPlace = (dir) => /[\\/]\.tasks[\\/]/.test(dir);
2231
+ async function traceMatrix(root, changeId, o = {}) {
2232
+ const ctx = await loadPlanContext(root, changeId, { requireApprovedDesign: false }).catch((e) => {
2233
+ throw new TestgenError("not-found", e.message);
2234
+ });
2235
+ const problems = [];
2236
+ const notStarted = /* @__PURE__ */ new Set();
2237
+ const requirements = requirementsOf(ctx);
2238
+ const stories = [];
2239
+ for (const id of await storyIds(ctx.dir))
2240
+ stories.push(await loadStory(ctx, id));
2241
+ const { manifests, problems: mp } = await readManifests(ctx.dir);
2242
+ problems.push(...mp);
2243
+ const manifestOf = new Map(manifests.map((m) => [m.story, m]));
2244
+ const changeRepos = await readChangeRepos(root, changeId);
2245
+ const repos = /* @__PURE__ */ new Set([
2246
+ ...changeRepos?.repos.map((r) => r.id) ?? [],
2247
+ ...stories.flatMap((s) => s.repos)
2248
+ ]);
2249
+ const generatedPaths = new Set(manifests.flatMap((m) => m.files.map((f) => f.path)));
2250
+ const restHops = ctx.hops.filter((h) => h.method && h.path?.startsWith("/"));
2251
+ const scenarioNames = requirements.filter((r) => r.scenario && r.scenario.split(/\s+/).length >= 2);
2252
+ const found = [];
2253
+ const dirs = /* @__PURE__ */ new Map();
2254
+ for (const repo of [...repos].sort()) {
2255
+ const storiesAt = /* @__PURE__ */ new Map();
2256
+ for (const s of stories.filter((x) => x.repos.includes(repo) || x.tasks.some((t) => t.repo === repo))) {
2257
+ const d = repoPlace(ctx, s, repo, changeRepos).dir;
2258
+ if (!d)
2259
+ continue;
2260
+ if (!storiesAt.has(d))
2261
+ storiesAt.set(d, /* @__PURE__ */ new Set());
2262
+ storiesAt.get(d).add(s.id);
2263
+ }
2264
+ const places = [...storiesAt.keys()];
2265
+ const fallback = repoPlace(ctx, { tasks: [] }, repo, changeRepos).dir;
2266
+ if (fallback && !places.includes(fallback))
2267
+ places.push(fallback);
2268
+ if (places[0])
2269
+ dirs.set(repo, fallback && !isTaskPlace(fallback) ? fallback : places[0]);
2270
+ const copies = /* @__PURE__ */ new Map();
2271
+ for (const dir of places) {
2272
+ for (const rel of await walk(dir)) {
2273
+ const text = (await readFile4(join7(dir, rel), "utf8").catch(() => "")).replace(/\r\n/g, "\n");
2274
+ if (text.length > 512 * 1024)
2275
+ continue;
2276
+ const key = `${rel}\0${text}`;
2277
+ const c = copies.get(key);
2278
+ if (c)
2279
+ c.dirs.push(dir);
2280
+ else
2281
+ copies.set(key, { rel, text, dirs: [dir] });
2282
+ }
2283
+ }
2284
+ for (const { rel, text, dirs: at } of copies.values()) {
2285
+ const refs = {
2286
+ stories: stories.filter((s) => new RegExp(`\\b${escape(s.id)}\\b`).test(text)).map((s) => s.id),
2287
+ scenarios: scenarioNames.filter((r) => text.toLowerCase().includes(r.scenario.toLowerCase())).map((r) => r.key),
2288
+ endpoints: [
2289
+ ...new Set(restHops.filter((h) => pathPattern(h.path).test(text)).map((h) => `${h.method} ${h.path}`))
2290
+ ]
2291
+ };
2292
+ if (!refs.stories.length && !refs.scenarios.length && !refs.endpoints.length)
2293
+ continue;
2294
+ const own = at.find((d) => refs.stories.some((id) => storiesAt.get(d)?.has(id)));
2295
+ const shared = at.find((d) => !isTaskPlace(d));
2296
+ const dir = own ?? shared ?? at[0];
2297
+ const path = posix2(relative2(root, join7(dir, rel)));
2298
+ if (generatedPaths.has(path))
2299
+ continue;
2300
+ found.push({ kind: "found", path, repo, refs });
2301
+ }
2302
+ }
2303
+ found.sort((a, b) => a.path.localeCompare(b.path));
2304
+ const capabilities = ctx.records.filter((r) => r.kind === "Capability");
2305
+ const rows = [];
2306
+ const gaps = [];
2307
+ const coveredReqs = /* @__PURE__ */ new Set();
2308
+ for (const s of stories) {
2309
+ const m = manifestOf.get(s.id);
2310
+ const gen = (m?.files ?? []).map((f) => ({
2311
+ kind: "generated",
2312
+ path: f.path,
2313
+ repo: f.repo,
2314
+ cases: f.cases
2315
+ }));
2316
+ const records = [
2317
+ .../* @__PURE__ */ new Set([
2318
+ ...s.hops.flatMap((h) => {
2319
+ const ep = endpointFor(ctx, h);
2320
+ const i = /^if-/.test(h.id) && ctx.get("Interface", h.id) ? [h.id] : [];
2321
+ return [
2322
+ ...i,
2323
+ ...ep ? [ep.id] : [],
2324
+ ...!i.length && !ep ? [`new: ${h.caller} \u2192 ${h.callee} ${h.operation.trim()}`] : []
2325
+ ];
2326
+ }),
2327
+ ...s.data.map((d) => ctx.get("Table", d.tableId) ? d.tableId : `new: table ${d.tableId}`)
2328
+ ])
2329
+ ];
2330
+ const ids = new Set(records);
2331
+ const features = capabilities.filter((c) => [...c.entryPoints, ...c.records].some((r) => ids.has(r))).map((c) => c.id).sort();
2332
+ const code = await codeOf(ctx, s, (r) => dirs.get(r), o.git, problems, notStarted);
2333
+ const storyFound = found.filter((f) => f.refs.stories.includes(s.id));
2334
+ for (const c of s.criteria) {
2335
+ const req = c.trace ? requirements.find((r) => r.key === `${c.trace.file}:${c.trace.line}`) : void 0;
2336
+ if (req)
2337
+ coveredReqs.add(req.key);
2338
+ const caseId = `acceptance.c${c.n}`;
2339
+ const linked = new Set((m?.cases ?? []).filter((x) => x.id === caseId || x.type !== "acceptance" && x.criterion === c.n).map((x) => x.id));
2340
+ const tests = [
2341
+ ...gen.filter((g) => g.cases.some((x) => linked.has(x))),
2342
+ ...found.filter((f) => req && f.refs.scenarios.includes(req.key) || f.refs.stories.includes(s.id))
2343
+ ];
2344
+ rows.push({
2345
+ ...req ? { requirement: req } : {},
2346
+ story: s.id,
2347
+ criterion: { n: c.n, text: c.text },
2348
+ tests,
2349
+ code,
2350
+ records,
2351
+ features
2352
+ });
2353
+ }
2354
+ if (s.tracesTo)
2355
+ for (const r of requirements.filter((x) => x.key === s.tracesTo || `${x.file}:${x.line}` === s.tracesTo))
2356
+ coveredReqs.add(r.key);
2357
+ const other = gen.filter((g) => !g.cases.every((x) => x.startsWith("acceptance.")) || !s.criteria.length);
2358
+ if (other.length || !s.criteria.length)
2359
+ rows.push({ story: s.id, tests: other, code, records, features });
2360
+ const hasTest = gen.length > 0 || storyFound.length > 0 || found.some((f) => f.refs.scenarios.some((k) => s.criteria.some((c) => c.trace && `${c.trace.file}:${c.trace.line}` === k)));
2361
+ if (!hasTest)
2362
+ gaps.push({
2363
+ kind: "story-without-test",
2364
+ subject: s.id,
2365
+ summary: `Story ${s.id} has no test: none generated and none in its repos names it.`,
2366
+ artifact: s.path,
2367
+ locator: { heading: s.title },
2368
+ evidence: [{ source: "artifact", path: s.path, line: 1 }]
2369
+ });
2370
+ const traced = s.criteria.some((c) => c.trace) || !!s.tracesTo;
2371
+ if (gen.length && !traced)
2372
+ gaps.push({
2373
+ kind: "test-without-requirement",
2374
+ subject: s.id,
2375
+ summary: `The tests of ${s.id} trace to no requirement: its criteria match no spec scenario.`,
2376
+ artifact: s.path,
2377
+ locator: { heading: s.title },
2378
+ evidence: [{ source: "artifact", path: s.path, line: 1 }]
2379
+ });
2380
+ }
2381
+ for (const r of requirements)
2382
+ if (!coveredReqs.has(r.key)) {
2383
+ rows.push({ requirement: r, tests: [], code: [], records: [], features: [] });
2384
+ gaps.push({
2385
+ kind: "requirement-without-story",
2386
+ subject: r.key,
2387
+ summary: `${r.scenario ? `Scenario "${r.scenario}" of requirement "${r.requirement}"` : `Requirement "${r.requirement}"`} has no story.`,
2388
+ artifact: r.file,
2389
+ locator: { line: r.line },
2390
+ evidence: [{ source: "artifact", path: r.file, line: r.line }]
2391
+ });
2392
+ }
2393
+ for (const f of found)
2394
+ if (!f.refs.stories.length && !f.refs.scenarios.length)
2395
+ gaps.push({
2396
+ kind: "test-without-requirement",
2397
+ subject: f.path,
2398
+ summary: `${f.path.split("/").pop()} in ${f.repo} tests ${f.refs.endpoints.join(", ")} but names no story or spec scenario.`,
2399
+ artifact: "tests",
2400
+ locator: { heading: f.path },
2401
+ evidence: [{ source: "code", repo: f.repo, path: f.path }]
2402
+ });
2403
+ for (const h of restHops.filter((x) => /^planned-(new|modify)$/.test(x.status))) {
2404
+ const p = normalizePath(h.path);
2405
+ const generated = manifests.some((m) => m.cases.some((c) => (c.type === "api" || c.type === "contract") && c.method === h.method && c.path && normalizePath(c.path) === p));
2406
+ const inRepo = found.some((f) => f.refs.endpoints.includes(`${h.method} ${h.path}`));
2407
+ if (generated || inRepo)
2408
+ continue;
2409
+ gaps.push({
2410
+ kind: "endpoint-without-test",
2411
+ subject: `${h.caller}-${h.callee}-${h.method}-${h.path}`,
2412
+ summary: `${h.method} ${h.path} (${h.caller} to ${h.callee}, ${h.status}) has no API or contract test.`,
2413
+ artifact: "interfaces.md",
2414
+ locator: { line: h.line },
2415
+ evidence: [{ source: "artifact", path: "interfaces.md", line: h.line }]
2416
+ });
2417
+ }
2418
+ if (notStarted.size)
2419
+ problems.push(`Not started yet (no task branch): ${[...notStarted].sort().join(", ")}.`);
2420
+ return {
2421
+ change: ctx.change.id,
2422
+ requirements,
2423
+ stories: stories.map((s) => s.id),
2424
+ rows,
2425
+ gaps: gaps.sort((a, b) => a.kind.localeCompare(b.kind) || a.subject.localeCompare(b.subject)),
2426
+ found,
2427
+ problems
2428
+ };
2429
+ }
2430
+ var RULES_FILE = fileURLToPath(new URL("../rules/trace.yaml", __atticus_import_meta_url));
2431
+ async function loadTraceRules() {
2432
+ const pack = ChecklistPack.parse((0, import_yaml3.parse)(await readFile4(RULES_FILE, "utf8")));
2433
+ return new Map(pack.rules.map((r) => [r.id, r]));
2434
+ }
2435
+ var FIX = {
2436
+ "requirement-without-story": "Plan a story for the scenario (atticus plan stories), or take the scenario out of the spec.",
2437
+ "story-without-test": "Generate its tests (atticus tests generate) or write one that names the story or its scenarios.",
2438
+ "endpoint-without-test": "Link the interface to a story and generate its API and contract tests, or write a test that calls it.",
2439
+ "test-without-requirement": "Trace the story to a spec scenario, add the requirement to the spec, or remove the test."
2440
+ };
2441
+ async function traceFindings(m, now) {
2442
+ const rules = await loadTraceRules();
2443
+ return m.gaps.map((g) => {
2444
+ const rule = rules.get(`trace.${g.kind}`);
2445
+ return Finding.parse({
2446
+ schemaVersion: 1,
2447
+ id: `${g.kind}-${slug(g.subject)}`.slice(0, 120).replace(/-+$/, ""),
2448
+ kind: "issue",
2449
+ change: m.change,
2450
+ gate: TRACE_GATE,
2451
+ round: 1,
2452
+ rule: rule.id,
2453
+ layer: rule.layer,
2454
+ lens: rule.lens,
2455
+ severity: rule.severity,
2456
+ artifact: g.artifact,
2457
+ locator: g.locator,
2458
+ summary: g.summary.length <= 200 ? g.summary : `${g.summary.slice(0, 197)}...`,
2459
+ why: rule.description ?? rule.title,
2460
+ fix: { text: FIX[g.kind] },
2461
+ evidence: g.evidence,
2462
+ waivable: false,
2463
+ state: "open",
2464
+ createdAt: now
2465
+ });
2466
+ });
2467
+ }
2468
+ async function writeTraceFindings(root, m, now) {
2469
+ return writeGateRound(root, m.change, TRACE_GATE, await traceFindings(m, now), now);
2470
+ }
2471
+ function cells(r) {
2472
+ return [
2473
+ r.requirement ? `${r.requirement.requirement} (${r.requirement.file}:${r.requirement.line})` : "",
2474
+ r.requirement?.scenario ?? "",
2475
+ r.story ?? "",
2476
+ r.criterion ? `${r.criterion.n}. ${r.criterion.text}` : r.story ? "(story)" : "",
2477
+ r.tests.map((t) => `${t.path}${t.kind === "found" ? " (found)" : ""}`).join("; "),
2478
+ r.code.join("; "),
2479
+ r.records.join("; "),
2480
+ r.features.join("; ")
2481
+ ];
2482
+ }
2483
+ var HEAD = [
2484
+ "Requirement",
2485
+ "Scenario",
2486
+ "Story",
2487
+ "Criterion",
2488
+ "Tests",
2489
+ "Code",
2490
+ "Records",
2491
+ "Features"
2492
+ ];
2493
+ function matrixMarkdown(m) {
2494
+ const md = (s) => s.replace(/\|/g, "\\|") || " ";
2495
+ const lines = [
2496
+ `# Traceability: ${m.change}`,
2497
+ "",
2498
+ `| ${HEAD.join(" | ")} |`,
2499
+ `| ${HEAD.map(() => "---").join(" | ")} |`,
2500
+ ...m.rows.map((r) => `| ${cells(r).map(md).join(" | ")} |`),
2501
+ "",
2502
+ m.gaps.length ? "## Gaps" : "No gaps.",
2503
+ ...m.gaps.length ? ["", ...m.gaps.map((g) => `- ${g.kind}: ${g.summary}`)] : []
2504
+ ];
2505
+ return `${lines.join("\n")}
2506
+ `;
2507
+ }
2508
+ function matrixCsv(m) {
2509
+ const q = (s) => /[",\n]/.test(s) ? `"${s.replace(/"/g, '""')}"` : s;
2510
+ return `${[HEAD.map((h) => h.toLowerCase()), ...m.rows.map(cells)].map((r) => r.map(q).join(",")).join("\n")}
2511
+ `;
2512
+ }
2513
+
2514
+ export {
2515
+ slug,
2516
+ generateTests,
2517
+ writeGateRound,
2518
+ traceMatrix,
2519
+ writeTraceFindings,
2520
+ matrixMarkdown,
2521
+ matrixCsv
2522
+ };