@maycombhq/atticus 2.0.0-beta.2 → 2.0.0-beta.22

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 (284) hide show
  1. package/dist/bin.js +8 -4
  2. package/dist/chunk-23XVSEV5.js +49 -0
  3. package/dist/{chunk-ZDPAS56H.js → chunk-24OXGB3P.js} +8 -2
  4. package/dist/{chunk-7EOBHUMC.js → chunk-24XLDU72.js} +4 -3
  5. package/dist/{chunk-CR3MTH32.js → chunk-2F4CYZKJ.js} +86 -11
  6. package/dist/{chunk-6IU67AEK.js → chunk-2GF7OLF3.js} +23 -15
  7. package/dist/chunk-2IIMFOPM.js +41 -0
  8. package/dist/chunk-2LGRMVCA.js +617 -0
  9. package/dist/{chunk-XPCM366L.js → chunk-2MMCFP5C.js} +10 -8
  10. package/dist/{chunk-JBTK5OP2.js → chunk-2Q56PQPS.js} +6 -4
  11. package/dist/{chunk-Z6LF7HQN.js → chunk-2Q5EDGEO.js} +7 -5
  12. package/dist/{chunk-3USYZGRF.js → chunk-2RLKFUUL.js} +38 -27
  13. package/dist/chunk-2SKB4O2E.js +59 -0
  14. package/dist/{chunk-SP2I63OO.js → chunk-34S2WGOB.js} +87 -4
  15. package/dist/{chunk-GQ4N4WKF.js → chunk-3NFFJL5D.js} +33 -27
  16. package/dist/{chunk-SJRGMG6A.js → chunk-3NWDLGG6.js} +28 -25
  17. package/dist/{chunk-4WBVUHXN.js → chunk-3RM4UK6W.js} +20 -18
  18. package/dist/chunk-42SIDKEU.js +583 -0
  19. package/dist/{chunk-GLUX4EEP.js → chunk-4EURQWQD.js} +20 -15
  20. package/dist/chunk-4U276ICE.js +39 -0
  21. package/dist/{chunk-ZMFPW66A.js → chunk-53T4O55J.js} +4 -3
  22. package/dist/{chunk-6J5OKA4Y.js → chunk-57TDNMI3.js} +17 -15
  23. package/dist/chunk-5BD2KYLE.js +57 -0
  24. package/dist/{chunk-7EFILTCI.js → chunk-5F7GFN2F.js} +28 -0
  25. package/dist/chunk-5NMKONLP.js +95 -0
  26. package/dist/{chunk-7ZUIMTOU.js → chunk-5SJZW52K.js} +26 -24
  27. package/dist/{chunk-4RS5KZQD.js → chunk-63YRMR6J.js} +6 -5
  28. package/dist/chunk-66PPNOON.js +62 -0
  29. package/dist/chunk-6H3Z5JHH.js +64 -0
  30. package/dist/{chunk-CEXSU4S6.js → chunk-6MUGR4LS.js} +775 -89
  31. package/dist/{chunk-QSXK2RGH.js → chunk-6OJQOPUN.js} +5 -3
  32. package/dist/{chunk-MVZVGYXH.js → chunk-6PPOWY5E.js} +84 -14
  33. package/dist/{chunk-7EKXJZ2Y.js → chunk-6ZGLAACW.js} +299 -38
  34. package/dist/chunk-6ZJ5A7AF.js +222 -0
  35. package/dist/{chunk-L24C55VN.js → chunk-745DAZ4L.js} +142 -30
  36. package/dist/{chunk-Q4D5X7QB.js → chunk-7ASNL4Q4.js} +46 -24
  37. package/dist/chunk-7BR7IPOH.js +35 -0
  38. package/dist/{chunk-7WD63HU2.js → chunk-7DOR27HL.js} +6 -6
  39. package/dist/chunk-7FTTA3HD.js +341 -0
  40. package/dist/{chunk-OLQJTJJU.js → chunk-7GJS6DNX.js} +12 -3
  41. package/dist/{chunk-7JUKDRSZ.js → chunk-7IGKHZ4Z.js} +347 -226
  42. package/dist/{chunk-G2B4ZQGH.js → chunk-7IOWA5D5.js} +11 -6
  43. package/dist/{chunk-EBEW3XXQ.js → chunk-7JITZFJC.js} +7 -5
  44. package/dist/chunk-7MJSQTL5.js +174 -0
  45. package/dist/{chunk-I4JRCD6M.js → chunk-7UOMVMFG.js} +25 -17
  46. package/dist/{chunk-YY6ZQQBP.js → chunk-AFWUDXEM.js} +19 -7
  47. package/dist/chunk-AFZ3QA6W.js +47 -0
  48. package/dist/chunk-AJQ72EBM.js +420 -0
  49. package/dist/chunk-AJYXNTNZ.js +317 -0
  50. package/dist/chunk-AVTBS4OW.js +49 -0
  51. package/dist/{chunk-FHH4I4ZV.js → chunk-BDKC6H5P.js} +12 -6
  52. package/dist/{chunk-IPKHE5D6.js → chunk-BEVTIBRH.js} +1526 -875
  53. package/dist/chunk-BHT7QK4G.js +162 -0
  54. package/dist/{chunk-6JUBZBMW.js → chunk-BIGDQDHD.js} +6 -4
  55. package/dist/{chunk-2BFCZ3RK.js → chunk-BO4KX37X.js} +20 -18
  56. package/dist/chunk-BPK5P77O.js +115 -0
  57. package/dist/chunk-BQ4SQQC6.js +59 -0
  58. package/dist/chunk-BQGDB6ZL.js +7342 -0
  59. package/dist/{chunk-2ANOROX3.js → chunk-BTGPE6CR.js} +11 -9
  60. package/dist/chunk-BTY3HZBO.js +93 -0
  61. package/dist/{chunk-DPZHZQKE.js → chunk-BXYMYEJ4.js} +44 -10
  62. package/dist/{chunk-2RMYG36Q.js → chunk-C3RY6KU6.js} +309 -346
  63. package/dist/chunk-C4R4JLHE.js +40 -0
  64. package/dist/chunk-CADWLE2T.js +18 -0
  65. package/dist/chunk-CBGGOQWU.js +82 -0
  66. package/dist/chunk-CMDXGQTS.js +47 -0
  67. package/dist/{chunk-MC3H7736.js → chunk-CQDGOPGH.js} +58 -18
  68. package/dist/{chunk-HFQCFSJR.js → chunk-CX4RZ2KS.js} +4 -3
  69. package/dist/{chunk-PAMPPUR6.js → chunk-CXJ6FUKN.js} +1852 -207
  70. package/dist/{chunk-ABWHYWNI.js → chunk-D73UEXYW.js} +3 -3
  71. package/dist/chunk-DCZWQLYT.js +1205 -0
  72. package/dist/chunk-DLOBCJL6.js +48 -0
  73. package/dist/{chunk-4BN53TG3.js → chunk-DOBQKHIF.js} +31 -23
  74. package/dist/chunk-E3ISYXO3.js +423 -0
  75. package/dist/{chunk-7SW2QWLU.js → chunk-E6ZIEVOI.js} +16 -13
  76. package/dist/chunk-EBL2R3YG.js +43 -0
  77. package/dist/{chunk-AIOQPLA5.js → chunk-EGQRREQH.js} +41 -15
  78. package/dist/chunk-EHA7RMZ3.js +58 -0
  79. package/dist/{chunk-M766ZJ6W.js → chunk-EIPTLPFC.js} +38 -27
  80. package/dist/chunk-EMRIQUND.js +517 -0
  81. package/dist/{chunk-DFBCSHVT.js → chunk-EPCJQQWD.js} +6 -4
  82. package/dist/{chunk-RZPYKDWS.js → chunk-F3P7N7SF.js} +1 -1
  83. package/dist/{chunk-R77KLRB3.js → chunk-FFLNS6VP.js} +21 -18
  84. package/dist/chunk-FFTRTL6R.js +26 -0
  85. package/dist/{chunk-IHMLFRVV.js → chunk-FPJPLKDW.js} +62 -16
  86. package/dist/{chunk-JCZIKA4C.js → chunk-FSMSFDLD.js} +5 -4
  87. package/dist/{chunk-AVJDSJ2F.js → chunk-FUJM6RTF.js} +8 -7
  88. package/dist/chunk-FZOJFH7Q.js +132 -0
  89. package/dist/{chunk-QRAOANID.js → chunk-G6X77UJZ.js} +23 -21
  90. package/dist/{chunk-3ALD2W2A.js → chunk-GCWRWSU7.js} +10 -4
  91. package/dist/chunk-GIW4ZVDB.js +39 -0
  92. package/dist/{chunk-B2WLAWNJ.js → chunk-GUX3PTYM.js} +3 -3
  93. package/dist/{chunk-DPUYDDQG.js → chunk-GZY6RUZ6.js} +25 -3
  94. package/dist/chunk-H4VO6WG3.js +10 -0
  95. package/dist/{chunk-MMOSZPWJ.js → chunk-H5HBGNXU.js} +5 -4
  96. package/dist/chunk-H7KM7LZE.js +229 -0
  97. package/dist/{chunk-AA6YULYB.js → chunk-HNM2YQT2.js} +7 -7
  98. package/dist/{chunk-3C7EYRFZ.js → chunk-HYU5VLGA.js} +1 -1
  99. package/dist/{chunk-XY3P7JW7.js → chunk-IDKCWSM5.js} +1032 -172
  100. package/dist/chunk-IFKEI4SA.js +237 -0
  101. package/dist/{chunk-F7HYOUFF.js → chunk-IGOE6KPG.js} +67 -22
  102. package/dist/{chunk-TDD55CJH.js → chunk-IH2XURVQ.js} +6 -5
  103. package/dist/chunk-INQHESFB.js +38 -0
  104. package/dist/chunk-IP74LCOQ.js +57 -0
  105. package/dist/chunk-IPHHDDC6.js +45 -0
  106. package/dist/{chunk-T6AIP7H5.js → chunk-ISUAAMGS.js} +110 -153
  107. package/dist/{chunk-A7UPUQFY.js → chunk-IUPG3UKI.js} +624 -319
  108. package/dist/{chunk-L6SGQIR3.js → chunk-IVBOQAGB.js} +7 -3
  109. package/dist/{chunk-27NKUS3X.js → chunk-J2JAKCIW.js} +2205 -796
  110. package/dist/{chunk-V2YTAIQO.js → chunk-J3IOLXWM.js} +24 -7
  111. package/dist/chunk-JA5FI3QV.js +142 -0
  112. package/dist/chunk-JAVI7JSP.js +49 -0
  113. package/dist/{chunk-HLJAXMM4.js → chunk-JB5LINEM.js} +6 -2
  114. package/dist/{chunk-2VHF7OXM.js → chunk-JENSEMXF.js} +1 -1
  115. package/dist/{chunk-6BVONWJ7.js → chunk-JJF6HALY.js} +4 -3
  116. package/dist/chunk-JLUCKAEL.js +38 -0
  117. package/dist/chunk-JRBFANFE.js +221 -0
  118. package/dist/{chunk-R6XF6UR3.js → chunk-KDNTAPLX.js} +4 -4
  119. package/dist/chunk-KFAVQZSX.js +33 -0
  120. package/dist/chunk-KJXD4PNL.js +50 -0
  121. package/dist/chunk-KN5BXMWN.js +44 -0
  122. package/dist/{chunk-PCVDV3UZ.js → chunk-KQGURXFU.js} +9 -7
  123. package/dist/{chunk-WY6PP6AA.js → chunk-KXITGYLF.js} +40 -6
  124. package/dist/{chunk-66YVAGGM.js → chunk-L27SV425.js} +22 -20
  125. package/dist/{chunk-NE2W3OO7.js → chunk-LDLL7LB3.js} +24 -22
  126. package/dist/chunk-LFGTYX5U.js +379 -0
  127. package/dist/{chunk-EYWIDJFU.js → chunk-LGBMTVNT.js} +7 -5
  128. package/dist/{chunk-Q5AMFEWD.js → chunk-LGFL54FP.js} +3 -2
  129. package/dist/{chunk-6TACQN3N.js → chunk-LJXMYKZJ.js} +7 -5
  130. package/dist/{chunk-BKVWTZ6A.js → chunk-LKOR5UET.js} +29 -26
  131. package/dist/chunk-LNBQOP7D.js +294 -0
  132. package/dist/{chunk-O3WI5KWW.js → chunk-LO6P2VYG.js} +2 -2
  133. package/dist/chunk-LQJPN536.js +122 -0
  134. package/dist/{chunk-YEDDOYSC.js → chunk-LYBNHMVZ.js} +29 -5
  135. package/dist/{chunk-A3GY6SOA.js → chunk-LZ7NUC62.js} +20 -18
  136. package/dist/chunk-M32BLWDH.js +37 -0
  137. package/dist/{chunk-VYDBJ73M.js → chunk-M3NANSDZ.js} +17 -15
  138. package/dist/chunk-MFHL5URL.js +31 -0
  139. package/dist/chunk-MFR5OPBD.js +109 -0
  140. package/dist/{chunk-DE76JROX.js → chunk-MG4ZL5RW.js} +32 -20
  141. package/dist/{chunk-N7IXEHK3.js → chunk-MO6P2JZH.js} +10 -8
  142. package/dist/{chunk-65NYPGRC.js → chunk-MUWDB4R4.js} +1 -1
  143. package/dist/chunk-MZCGSXVR.js +34 -0
  144. package/dist/{chunk-MGYZM27B.js → chunk-N3XIYPU3.js} +51 -23
  145. package/dist/chunk-NDCF4R6S.js +172 -0
  146. package/dist/{chunk-DHPACDG7.js → chunk-NR2UJWOC.js} +10 -8
  147. package/dist/chunk-NXJ5URP6.js +51 -0
  148. package/dist/{chunk-UNM6ZOKP.js → chunk-NY2XSR5K.js} +147 -18
  149. package/dist/{chunk-5GDRCDQB.js → chunk-O45HFTFS.js} +3 -2
  150. package/dist/{chunk-ONLS5QA5.js → chunk-O4GXGGBX.js} +14 -12
  151. package/dist/chunk-O5PXQR67.js +249 -0
  152. package/dist/{chunk-PRPPGYZG.js → chunk-O66DM6DJ.js} +6 -4
  153. package/dist/{chunk-KEM4SDDP.js → chunk-O7FDLW7L.js} +11 -8
  154. package/dist/{chunk-T2UYZSCQ.js → chunk-O7XLYEFA.js} +169 -29
  155. package/dist/{chunk-FITWGOJ2.js → chunk-OHNCWBKF.js} +4 -4
  156. package/dist/{chunk-652QWTLZ.js → chunk-OHVTTOPS.js} +3 -3
  157. package/dist/chunk-OMI6FPPN.js +43 -0
  158. package/dist/chunk-ONUPY3H6.js +53 -0
  159. package/dist/chunk-OTLDSZDV.js +69 -0
  160. package/dist/chunk-OUBGGKDU.js +22 -0
  161. package/dist/chunk-OWNCM4ZN.js +330 -0
  162. package/dist/{chunk-YISXLPHN.js → chunk-P2V7QW4D.js} +235 -35
  163. package/dist/{chunk-CNUKCS2T.js → chunk-P32RUW3B.js} +1632 -93
  164. package/dist/{chunk-BWZ77RQT.js → chunk-PAQ2AS5E.js} +1 -1
  165. package/dist/{chunk-ZMAINWBW.js → chunk-PBPBMP2B.js} +4 -3
  166. package/dist/{chunk-ZVM6AKKM.js → chunk-PJSJPMJI.js} +3 -3
  167. package/dist/{chunk-EFHQ3TWP.js → chunk-PKBHS4AA.js} +233 -56
  168. package/dist/{chunk-GT4I4ZWM.js → chunk-PMUAJGLI.js} +20 -16
  169. package/dist/chunk-PPD7HTDP.js +152 -0
  170. package/dist/chunk-PSOFBQWB.js +42 -0
  171. package/dist/{chunk-S2A7YC3Q.js → chunk-Q2TECDIT.js} +10694 -8896
  172. package/dist/chunk-Q4K5WB2W.js +42 -0
  173. package/dist/{chunk-UGLN7QHV.js → chunk-Q6DFNVRJ.js} +14 -12
  174. package/dist/chunk-QA4O2GGC.js +265 -0
  175. package/dist/chunk-QAXW2N3B.js +1216 -0
  176. package/dist/chunk-QBNWF5ZU.js +19 -0
  177. package/dist/chunk-QFWA2DPH.js +224 -0
  178. package/dist/{chunk-23W7VWKR.js → chunk-QKUJMING.js} +1363 -114
  179. package/dist/{chunk-NXFPS2LR.js → chunk-R44CYFPV.js} +16 -13
  180. package/dist/{chunk-CKSITVS4.js → chunk-R7NYHMUU.js} +2687 -786
  181. package/dist/chunk-REWWDVQB.js +162 -0
  182. package/dist/{chunk-JMSCMUCA.js → chunk-RGERPOIV.js} +145 -21
  183. package/dist/{chunk-6JCPI55B.js → chunk-RLDGWLQ7.js} +49 -7
  184. package/dist/chunk-RUGXBPWI.js +384 -0
  185. package/dist/{chunk-CPA6HPVR.js → chunk-RXPJMEOW.js} +18 -12
  186. package/dist/{chunk-3HI6Z55Y.js → chunk-S4PMJU6H.js} +339 -43
  187. package/dist/chunk-S5LSDXV4.js +40 -0
  188. package/dist/chunk-S7FCV7S5.js +51 -0
  189. package/dist/{chunk-5GGOLAKC.js → chunk-SDKLKB6I.js} +28 -26
  190. package/dist/chunk-SFSPGFCV.js +62 -0
  191. package/dist/{chunk-ZNCEKR4J.js → chunk-SFUP7TTO.js} +12 -8
  192. package/dist/{chunk-64EQYWCA.js → chunk-SLXZOMA6.js} +69 -19
  193. package/dist/chunk-SQV37XO2.js +273 -0
  194. package/dist/{chunk-K65BJIMT.js → chunk-SU74NDMQ.js} +19 -17
  195. package/dist/chunk-SYV5LHML.js +165 -0
  196. package/dist/chunk-T4YPZIQN.js +36 -0
  197. package/dist/chunk-T6DQR5TV.js +323 -0
  198. package/dist/{chunk-JIITQXFB.js → chunk-THDMSZ3R.js} +3075 -802
  199. package/dist/{chunk-JGZCG5VM.js → chunk-TNR75OXT.js} +7 -7
  200. package/dist/{chunk-D3PO4DOZ.js → chunk-TUULXQRW.js} +174 -15
  201. package/dist/chunk-TYQBFTBR.js +516 -0
  202. package/dist/chunk-U76OWS3Q.js +49 -0
  203. package/dist/{chunk-YYQN46FY.js → chunk-UCROWG2D.js} +27 -24
  204. package/dist/{chunk-6M5NK3K3.js → chunk-UF4AZOZB.js} +50 -10
  205. package/dist/chunk-UF4GLFBX.js +345 -0
  206. package/dist/{chunk-OT2O76OS.js → chunk-UGUP3ZM6.js} +240 -39
  207. package/dist/chunk-UNAGJYET.js +45 -0
  208. package/dist/{chunk-5WG5YP4T.js → chunk-UOR2DTHH.js} +16 -14
  209. package/dist/{chunk-S3QNRWYR.js → chunk-UPOY4YSQ.js} +14 -12
  210. package/dist/{chunk-EPTBZ3GP.js → chunk-UPS2DGFC.js} +1 -1
  211. package/dist/{chunk-4AK7IBVP.js → chunk-UUU4CLJG.js} +4 -2
  212. package/dist/{chunk-W3BQAZ3Q.js → chunk-UWMJKX5U.js} +14 -12
  213. package/dist/{chunk-QZSX3F7C.js → chunk-UWOJLHFT.js} +27 -23
  214. package/dist/chunk-VBTP56CE.js +535 -0
  215. package/dist/chunk-VCTWPLAO.js +211 -0
  216. package/dist/chunk-VNU26RCO.js +1215 -0
  217. package/dist/{chunk-CSZ4C5N6.js → chunk-VTZMAJ3W.js} +93 -10
  218. package/dist/{chunk-TXLH6WHQ.js → chunk-VW62D6QE.js} +14 -12
  219. package/dist/{chunk-CAQZUNOH.js → chunk-VY72R2XZ.js} +610 -3
  220. package/dist/{chunk-2DIQQJCB.js → chunk-VZALIKIF.js} +18 -15
  221. package/dist/{chunk-TYAID7B7.js → chunk-W62H2XSO.js} +25 -23
  222. package/dist/{chunk-HVUPKCGJ.js → chunk-W7VWI2YH.js} +9 -6
  223. package/dist/{chunk-TANFV3MZ.js → chunk-WERM6KPJ.js} +7 -4
  224. package/dist/{chunk-BOHJZTEQ.js → chunk-WF7B67YE.js} +208 -87
  225. package/dist/{chunk-7FBGJFEE.js → chunk-WG5U3F5V.js} +14 -12
  226. package/dist/{chunk-4NDAAFQT.js → chunk-WJF2CS6M.js} +8 -5
  227. package/dist/{chunk-XAKWMCXI.js → chunk-WKUJ3KPV.js} +4 -4
  228. package/dist/chunk-WMTRHUI5.js +103 -0
  229. package/dist/chunk-WNKPRSGI.js +36 -0
  230. package/dist/chunk-WXFLMQSR.js +223 -0
  231. package/dist/{chunk-RRTKKSLQ.js → chunk-X3WY6MN3.js} +8 -6
  232. package/dist/{chunk-ZCJDOK2S.js → chunk-X4CXA2IL.js} +47 -23
  233. package/dist/{chunk-UBBC2XSG.js → chunk-X5F3Q5LV.js} +560 -105
  234. package/dist/{chunk-C6EKFH7J.js → chunk-XD75VZIH.js} +9 -7
  235. package/dist/chunk-XERYFDB5.js +297 -0
  236. package/dist/chunk-XI2LCCYQ.js +420 -0
  237. package/dist/chunk-Y2EQNWKV.js +933 -0
  238. package/dist/{chunk-LR3R6Z2A.js → chunk-YHCMZC2J.js} +7 -7
  239. package/dist/chunk-YYY3AZFE.js +122 -0
  240. package/dist/{chunk-GDLULHTC.js → chunk-Z5IA3WDR.js} +19 -31
  241. package/dist/{chunk-T6XQL73F.js → chunk-ZFFYNRIO.js} +63 -23
  242. package/dist/{chunk-EDS2L2LD.js → chunk-ZFL7NY4F.js} +56 -27
  243. package/dist/{chunk-YMZ3TNYB.js → chunk-ZGULQAPZ.js} +7 -4
  244. package/dist/{chunk-4SKZDI62.js → chunk-ZIUFM5HT.js} +3 -1
  245. package/dist/{chunk-DKJA76CJ.js → chunk-ZJFRMSJT.js} +15 -13
  246. package/dist/chunk-ZJXLNDH4.js +239 -0
  247. package/dist/chunk-ZQ3ON2VQ.js +252 -0
  248. package/dist/{chunk-HITVBIJS.js → chunk-ZQ7SKESZ.js} +150 -415
  249. package/dist/{chunk-6QSERWXH.js → chunk-ZQXZ2SES.js} +24 -21
  250. package/dist/chunk-ZX57KSEQ.js +39 -0
  251. package/dist/{chunk-I3E7CX4H.js → chunk-ZZK56BLP.js} +5 -3
  252. package/dist/files.json +664 -368
  253. package/dist/packages/naysayer/constitution/constitution.yaml +19 -0
  254. package/dist/packages/sdk/npm/index.js +6194 -2542
  255. package/dist/packages/ui/dist/assets/{elk-api-tma0MCuh.js → elk-api-CIf-0gz5.js} +2 -2
  256. package/dist/packages/ui/dist/assets/index-CoK_Dl0D.js +47 -0
  257. package/dist/packages/ui/dist/assets/index-Dw20IRhQ.css +1 -0
  258. package/dist/packages/ui/dist/index.html +3 -2
  259. package/package.json +5 -5
  260. package/dist/chunk-2BF2EYJV.js +0 -174
  261. package/dist/chunk-42EVSKME.js +0 -37
  262. package/dist/chunk-4BKGMUIK.js +0 -53
  263. package/dist/chunk-5KLGBXG5.js +0 -48
  264. package/dist/chunk-6BNC6EQ4.js +0 -41
  265. package/dist/chunk-6D2CQ5II.js +0 -41
  266. package/dist/chunk-7AVHLZGD.js +0 -45
  267. package/dist/chunk-D46WUV3T.js +0 -33
  268. package/dist/chunk-E36QOJMD.js +0 -47
  269. package/dist/chunk-FQXYPF7E.js +0 -147
  270. package/dist/chunk-GBRFVT63.js +0 -45
  271. package/dist/chunk-ICY36XCH.js +0 -34
  272. package/dist/chunk-J4K3KVLD.js +0 -131
  273. package/dist/chunk-JZU3H6UQ.js +0 -54
  274. package/dist/chunk-NN2ZQEB4.js +0 -38
  275. package/dist/chunk-PCOCACY3.js +0 -385
  276. package/dist/chunk-RTEYOSJY.js +0 -29
  277. package/dist/chunk-RUVFXE2L.js +0 -666
  278. package/dist/chunk-RVH5SUW5.js +0 -69
  279. package/dist/chunk-VN3DP4LQ.js +0 -43
  280. package/dist/chunk-W5BVQF44.js +0 -36
  281. package/dist/chunk-WVPJSRRJ.js +0 -52
  282. package/dist/chunk-XPTOBS2D.js +0 -48
  283. package/dist/packages/ui/dist/assets/index-B54fm9FQ.js +0 -29
  284. package/dist/packages/ui/dist/assets/index-DD_hufQz.css +0 -1
@@ -7,18 +7,19 @@ import {
7
7
  featuresForScan,
8
8
  namingHint,
9
9
  syncSources
10
- } from "./chunk-L24C55VN.js";
10
+ } from "./chunk-745DAZ4L.js";
11
11
  import {
12
12
  dirSource,
13
+ rereadPlan,
13
14
  sameCommit,
14
15
  verifyFacts
15
- } from "./chunk-PAMPPUR6.js";
16
+ } from "./chunk-CXJ6FUKN.js";
16
17
  import {
17
18
  scanInputs
18
- } from "./chunk-23W7VWKR.js";
19
+ } from "./chunk-QKUJMING.js";
19
20
  import {
20
21
  listIngestors
21
- } from "./chunk-7EKXJZ2Y.js";
22
+ } from "./chunk-6ZGLAACW.js";
22
23
  import {
23
24
  BUILT_IN_EXTRACTORS,
24
25
  MODEL_READERS,
@@ -33,12 +34,7 @@ import {
33
34
  runDialectTests,
34
35
  runFrameworkTests,
35
36
  unmodelledFrameworks
36
- } from "./chunk-MC3H7736.js";
37
- import {
38
- baseClonePath,
39
- changesSince,
40
- checkoutState
41
- } from "./chunk-EFHQ3TWP.js";
37
+ } from "./chunk-CQDGOPGH.js";
42
38
  import {
43
39
  DEFAULT_IGNORES,
44
40
  configHash,
@@ -50,509 +46,321 @@ import {
50
46
  sandboxedExtractor,
51
47
  scanRepo,
52
48
  withParseCache
53
- } from "./chunk-GQ4N4WKF.js";
49
+ } from "./chunk-3NFFJL5D.js";
50
+ import {
51
+ baseClonePath,
52
+ changesSince,
53
+ checkoutState,
54
+ inPlaceDir
55
+ } from "./chunk-PKBHS4AA.js";
54
56
  import {
55
57
  buildContextPack,
56
58
  toRunSummary
57
- } from "./chunk-CNUKCS2T.js";
59
+ } from "./chunk-P32RUW3B.js";
58
60
  import {
59
61
  AgentDefinition,
60
62
  DECLARED_CALLER,
61
63
  DEFAULT_SPEC_TEMPLATE,
62
64
  HOSTS_NONE,
65
+ HOSTS_OUTSIDE_INTERNAL,
66
+ HOSTS_OUTSIDE_VENDOR,
63
67
  IsoDateTime,
64
68
  NEVER_CHECKED_STEPS,
69
+ NO_TARGET,
65
70
  PluginExpected,
66
71
  REPO_TYPE_SCHEMA_VERSION,
67
72
  RecordId,
68
73
  SCAN_SETTINGS_SCHEMA_VERSION,
74
+ TARGETS_ANOTHER_LABEL,
75
+ TARGETS_SPLIT_LABEL,
76
+ TARGETS_STEP_ID,
77
+ TOPICS_OUTSIDE_LABEL,
78
+ TOPICS_STEP_ID,
69
79
  applyCurated,
80
+ applyLinkEvidence,
81
+ applySharedMaps,
82
+ callHostOf,
70
83
  callTargetQuestion,
71
84
  callerRecords,
72
85
  catalogDirOf,
86
+ cmpNatural,
87
+ cmpText,
88
+ conventionsOf,
73
89
  declaredCallsOf,
74
90
  deserializeRecord,
91
+ effectiveComponentTypes,
75
92
  external_exports,
76
93
  factIdPart,
77
94
  factsDirOf,
78
95
  factsRelOf,
79
96
  fieldKey,
80
97
  flowFiles,
98
+ helperOn,
81
99
  hostMatches,
82
100
  hostQuestionText,
83
101
  identifierWords,
102
+ isAiBy,
103
+ isGuessedJoin,
104
+ isPathMatch,
84
105
  isPrototypeType,
85
106
  joinCallers,
107
+ joinConventions,
86
108
  joinFacts,
109
+ joinFlagsOf,
110
+ joinGuessesOf,
87
111
  joinRecords,
112
+ joinSharedMaps,
113
+ joinTerms,
88
114
  lineNamesField,
89
115
  lineNamesRoute,
90
116
  lineNamesWord,
117
+ linkEvidenceOf,
91
118
  listQuestions,
92
119
  loadOrgPack,
93
120
  mergeCatalog,
94
121
  normalizePath,
95
122
  orgPackDirOf,
123
+ outsideSystemOf,
124
+ pairsFromCorrections,
125
+ parseCorrections,
96
126
  parseFacts,
97
127
  parseRecord,
98
128
  projectFacts,
99
129
  questionId,
100
130
  readParsedFiles,
101
131
  recordPath,
102
- require_dist,
103
132
  serializeRecord,
133
+ sharedJoinAbout,
134
+ singularNoun,
104
135
  slugify,
105
136
  specFileHash,
106
137
  specFindingFingerprint,
138
+ stackHelpersOff,
139
+ stripLinkEvidence,
140
+ stripSharedMaps,
141
+ termGrantRecords,
107
142
  throwIfCancelled,
108
143
  toCanonicalYaml,
144
+ topicOfQuestion,
145
+ topicQuestionText,
109
146
  urlHostPort
110
- } from "./chunk-S2A7YC3Q.js";
147
+ } from "./chunk-Q2TECDIT.js";
111
148
  import {
112
149
  SCHEMA_VERSION,
113
150
  writeFileAtomic
114
- } from "./chunk-D3PO4DOZ.js";
151
+ } from "./chunk-TUULXQRW.js";
152
+ import {
153
+ require_dist
154
+ } from "./chunk-BQGDB6ZL.js";
115
155
  import {
116
156
  __toESM
117
157
  } from "./chunk-TV45EUGM.js";
118
158
 
119
- // packages/glossary/dist/proposals.js
120
- var import_yaml = __toESM(require_dist(), 1);
121
- import { mkdir, readdir, readFile, rm, writeFile } from "node:fs/promises";
122
- import { join } from "node:path";
123
- var GLOSSARY_DEFINER = "glossary-definer";
124
- var GLOSSARY_CHANGE = "glossary";
125
- var GLOSSARY_OUTPUT = "catalog/proposals/glossary-definitions.yaml";
126
- var DefinitionProposalFile = external_exports.strictObject({
127
- schemaVersion: external_exports.literal(1),
128
- term: RecordId,
129
- definition: external_exports.string().min(1),
130
- evidence: external_exports.array(external_exports.string().min(1)).min(1),
131
- agent: RecordId,
132
- runId: external_exports.string().optional(),
133
- at: IsoDateTime
134
- });
135
- var LEADING = ["schemaVersion", "term", "definition", "evidence"];
136
- async function writeDefinitionProposals(dir, proposals) {
137
- const written = [];
138
- if (!proposals.length)
139
- return written;
140
- await mkdir(join(dir, "glossaryterm"), { recursive: true });
141
- for (const p of proposals) {
142
- const file = `glossaryterm/${p.term}.yaml`;
143
- const body = DefinitionProposalFile.parse({ schemaVersion: 1, ...p });
144
- await writeFile(join(dir, file), toCanonicalYaml(body, LEADING), "utf8");
145
- written.push(file);
146
- }
147
- return written;
159
+ // packages/scanner/dist/scan.js
160
+ import { stat } from "node:fs/promises";
161
+ import { dirname as dirname3, isAbsolute, join as join9, resolve as resolve2 } from "node:path";
162
+ import { fileURLToPath } from "node:url";
163
+
164
+ // packages/glossary/dist/words.js
165
+ var STOP_TOKENS = /* @__PURE__ */ new Set([
166
+ "a",
167
+ "an",
168
+ "and",
169
+ "api",
170
+ "at",
171
+ "by",
172
+ "create",
173
+ "created",
174
+ "delete",
175
+ "for",
176
+ "get",
177
+ "id",
178
+ "ids",
179
+ "in",
180
+ "is",
181
+ "list",
182
+ "of",
183
+ "on",
184
+ "or",
185
+ "patch",
186
+ "post",
187
+ "put",
188
+ "the",
189
+ "to",
190
+ "ts",
191
+ "update",
192
+ "updated",
193
+ "uuid"
194
+ ]);
195
+ var QUALIFIERS = /* @__PURE__ */ new Set([
196
+ "cur",
197
+ "curr",
198
+ "current",
199
+ "has",
200
+ "max",
201
+ "min",
202
+ "new",
203
+ "old",
204
+ "prev",
205
+ "previous"
206
+ ]);
207
+ var splitWords = (s) => identifierWords(s);
208
+ var singular = (w) => singularNoun(w);
209
+ var isNoise = (w) => STOP_TOKENS.has(w) || /^v\d+$/.test(w) || /^\d+$/.test(w);
210
+ function businessWords(s) {
211
+ return splitWords(s).filter((w) => !isNoise(w)).map(singular);
148
212
  }
149
- async function readDefinitionProposals(dir) {
150
- const proposals = [];
151
- const problems = [];
152
- let files;
153
- try {
154
- files = (await readdir(join(dir, "glossaryterm"))).filter((f) => /\.ya?ml$/.test(f)).sort();
155
- } catch {
156
- return { proposals, problems };
157
- }
158
- for (const f of files) {
159
- const file = `glossaryterm/${f}`;
160
- let data;
161
- try {
162
- data = (0, import_yaml.parse)(await readFile(join(dir, file), "utf8"));
163
- } catch (e) {
164
- problems.push({ file, message: `invalid YAML: ${e.message.split("\n")[0]}` });
165
- continue;
166
- }
167
- const p = DefinitionProposalFile.safeParse(data);
168
- if (!p.success) {
169
- problems.push({
170
- file,
171
- message: p.error.issues.map((i) => `${i.path.join(".")}: ${i.message}`).join("; ")
172
- });
173
- continue;
174
- }
175
- const { schemaVersion: _v, ...rest } = p.data;
176
- void _v;
177
- proposals.push(rest);
178
- }
179
- return { proposals, problems };
213
+ function compoundOf(name) {
214
+ const w = splitWords(name).filter((x) => !/^\d+$/.test(x));
215
+ while (w.length && (isNoise(w[0]) || QUALIFIERS.has(w[0])))
216
+ w.shift();
217
+ while (w.length && isNoise(w.at(-1)))
218
+ w.pop();
219
+ if (w.length < 2 || w.some(isNoise))
220
+ return void 0;
221
+ return [...w.slice(0, -1), singular(w.at(-1))].join(" ");
180
222
  }
181
- function glossaryDefinerAgent(orgPack) {
182
- const own = orgPack?.agents.find((a) => a.id === GLOSSARY_DEFINER);
183
- if (own)
184
- return own;
185
- return AgentDefinition.parse({
186
- schemaVersion: 1,
187
- id: GLOSSARY_DEFINER,
188
- role: "business-analyst",
189
- description: "Proposes a one-sentence business definition for each glossary term found only in code, from where the code uses it.",
190
- inputs: ["glossary", "catalog-slice"],
191
- // C5 has no output kind for catalog proposals (see ADR 0031); a definition seeds specs.
192
- outputArtifact: "specs",
193
- tokenBudget: orgPack?.runtime.budgets.defaultTokens ?? 15e4,
194
- allowedRuntimes: ["hand-off", "model", "headless-claude-code", "mock", "replay"]
195
- });
223
+ function singleOf(name) {
224
+ const w = businessWords(name).filter((x) => !QUALIFIERS.has(x));
225
+ return w.length === 1 ? w[0] : void 0;
196
226
  }
197
- var INSTRUCTIONS = `# Glossary definer
198
-
199
- You propose business definitions for words an estate's code uses that no wiki page defines. Each term below is a data block: its id, the word, and the records (endpoints, topics, tables, columns, schema fields) that use it. For each term propose:
200
-
201
- - definition: one sentence a product owner would accept, saying what the word means to the business, only from the evidence;
202
- - evidence: the record ids from that term's block your definition relies on.
203
-
204
- Don't invent systems, rules or numbers. A proposal that cites no record id from its term is discarded. Write YAML and nothing else:
205
-
206
- terms:
207
- - id: <term id>
208
- definition: <one sentence>
209
- evidence: [<record id>, ...]`;
210
- var Proposal = external_exports.object({
211
- id: external_exports.string().min(1),
212
- definition: external_exports.string().trim().min(3).max(400),
213
- evidence: external_exports.array(external_exports.coerce.string()).default([])
214
- });
215
- function block(e) {
216
- return toCanonicalYaml({
217
- id: e.id,
218
- term: e.term,
219
- usedBy: (e.code?.uses ?? []).map((u) => ({
220
- id: u.record,
221
- kind: u.kind,
222
- name: u.name,
223
- ...u.evidence.path ? {
224
- code: `${u.evidence.repo ? `${u.evidence.repo}:` : ""}${u.evidence.path}${u.evidence.line ? `:${u.evidence.line}` : ""}`
225
- } : {}
226
- }))
227
- }, ["id", "term", "usedBy", "id", "kind", "name", "code"]).trimEnd();
227
+ function termKey(term) {
228
+ const w = term.toLowerCase().replace(/[`*_"'’]/g, "").split(/[^a-z0-9]+/).filter(Boolean);
229
+ if (!w.length)
230
+ return "";
231
+ return [...w.slice(0, -1), singular(w.at(-1))].join(" ");
228
232
  }
229
- function parseDefinerOutput(output, entries, meta) {
230
- let data;
231
- try {
232
- data = (0, import_yaml.parse)(output.replace(/^```(?:ya?ml)?\s*\n|\n```\s*$/g, ""));
233
- } catch (e) {
234
- return {
235
- proposals: [],
236
- discarded: [{ reason: `the output is not YAML: ${e.message.split("\n")[0]}` }]
237
- };
238
- }
239
- const list = data?.terms;
240
- if (!Array.isArray(list))
241
- return { proposals: [], discarded: [{ reason: "the output has no terms list" }] };
242
- const byId = new Map(entries.map((e) => [e.id, e]));
243
- const out = { proposals: [], discarded: [] };
244
- for (const raw of list) {
245
- const p = Proposal.safeParse(raw);
246
- const id = typeof raw?.id === "string" ? raw.id : void 0;
247
- if (!p.success) {
248
- out.discarded.push({ ...id ? { term: id } : {}, reason: "incomplete proposal" });
249
- continue;
250
- }
251
- const e = byId.get(p.data.id);
252
- if (!e) {
253
- out.discarded.push({
254
- term: p.data.id,
255
- reason: `${p.data.id} isn't one of the terms it was given`
256
- });
257
- continue;
258
- }
259
- if (out.proposals.some((x) => x.term === e.id)) {
260
- out.discarded.push({ term: e.id, reason: "a second proposal for the same term" });
261
- continue;
262
- }
263
- const known = new Set((e.code?.uses ?? []).map((u) => u.record));
264
- const evidence = [...new Set(p.data.evidence.filter((x) => known.has(x)))];
265
- if (!evidence.length) {
266
- out.discarded.push({ term: e.id, reason: "it cites no record that uses the term" });
267
- continue;
268
- }
269
- out.proposals.push({
270
- term: e.id,
271
- definition: p.data.definition.replace(/\s+/g, " "),
272
- evidence,
273
- agent: meta.agent,
274
- at: meta.at,
275
- ...meta.runId ? { runId: meta.runId } : {}
276
- });
277
- }
278
- return out;
233
+ function termId(term) {
234
+ return `term.${termKey(term).replace(/\s+/g, "-")}`.slice(0, 120);
279
235
  }
280
- function undefinedCodeTerms(entries) {
281
- return entries.filter((e) => e.sources.length === 1 && e.sources[0] === "code" && (e.code?.uses.length ?? 0) > 0);
236
+ function displayTerm(term) {
237
+ const t = term.trim().replace(/\s+/g, " ");
238
+ return t.charAt(0).toUpperCase() + t.slice(1);
282
239
  }
283
- async function proposeDefinitions(entries, runtime, o = {}) {
284
- const todo = [...entries].sort((a, b) => a.id.localeCompare(b.id));
285
- if (!todo.length)
286
- return { proposals: [], discarded: [] };
287
- const agent = glossaryDefinerAgent(o.orgPack);
288
- const now2 = o.now ?? /* @__PURE__ */ new Date();
289
- const at3 = now2.toISOString().replace(/\.\d{3}Z$/, "Z");
290
- const levels = o.orgPack?.pack?.classification ?? [
291
- "public",
292
- "internal",
293
- "confidential",
294
- "restricted"
295
- ];
296
- const sources = [
297
- {
298
- id: "0-instructions",
299
- kind: "instruction",
300
- origin: "atticus/glossary",
301
- text: INSTRUCTIONS,
302
- classification: "public",
303
- trusted: true
304
- },
305
- ...todo.map((e) => ({
306
- id: e.id,
307
- kind: "catalog",
308
- origin: `catalog/generated/glossaryterm/${e.id}.yaml`,
309
- text: block(e),
310
- classification: "internal",
311
- priority: 10
312
- }))
313
- ];
314
- const pack = buildContextPack({
315
- change: GLOSSARY_CHANGE,
316
- agent: agent.id,
317
- budget: o.orgPack?.runtime.budgets.perAgent[agent.id] ?? agent.tokenBudget,
318
- sources,
319
- levels,
320
- maxClassification: levels.includes("internal") ? "internal" : levels.at(-1),
321
- now: now2
322
- });
323
- const sent = todo.filter((e) => pack.manifest.sources.some((s) => s.id === e.id));
324
- const discarded = todo.filter((e) => !sent.includes(e)).map((e) => ({
325
- term: e.id,
326
- reason: "left out of the context pack (budget or classification)"
327
- }));
328
- const prepared = await runtime.prepare({
329
- change: GLOSSARY_CHANGE,
330
- agent,
331
- pack,
332
- outputPath: GLOSSARY_OUTPUT
333
- });
334
- const run = await runtime.run(prepared.runId);
335
- if (run.state === "waiting" || run.state === "prepared")
336
- return {
337
- proposals: [],
338
- discarded,
339
- waiting: run.runId,
340
- promptSha256: pack.manifest.promptSha256
341
- };
342
- if (run.state !== "validated" || run.output === void 0)
343
- return {
344
- proposals: [],
345
- discarded: [
346
- ...discarded,
347
- { reason: `the glossary definer ${run.state}: ${run.errors.join("; ") || "no output"}` }
348
- ],
349
- run: toRunSummary(run),
350
- promptSha256: pack.manifest.promptSha256
351
- };
352
- const parsed = parseDefinerOutput(run.output, sent, { agent: agent.id, at: at3, runId: run.runId });
353
- return {
354
- proposals: parsed.proposals,
355
- discarded: [...discarded, ...parsed.discarded],
356
- run: toRunSummary(run),
357
- promptSha256: pack.manifest.promptSha256
358
- };
240
+ function definitionKey(d) {
241
+ return d.toLowerCase().replace(/[^a-z0-9]+/g, " ").trim();
359
242
  }
360
243
 
361
- // packages/glossary/dist/decisions.js
362
- import { createHash } from "node:crypto";
363
- import { mkdir as mkdir2, writeFile as writeFile2 } from "node:fs/promises";
364
- import { dirname, join as join2 } from "node:path";
365
-
366
- // packages/glossary/dist/words.js
367
- var STOP_TOKENS = /* @__PURE__ */ new Set([
368
- "a",
369
- "an",
370
- "and",
371
- "api",
372
- "at",
373
- "by",
374
- "create",
375
- "created",
376
- "delete",
377
- "for",
378
- "get",
379
- "id",
380
- "ids",
381
- "in",
382
- "is",
383
- "list",
384
- "of",
385
- "on",
386
- "or",
387
- "patch",
388
- "post",
389
- "put",
390
- "the",
391
- "to",
392
- "ts",
393
- "update",
394
- "updated",
395
- "uuid"
396
- ]);
397
- var QUALIFIERS = /* @__PURE__ */ new Set([
398
- "cur",
399
- "curr",
400
- "current",
401
- "has",
402
- "max",
403
- "min",
404
- "new",
405
- "old",
406
- "prev",
407
- "previous"
408
- ]);
409
- var splitWords = (s) => identifierWords(s);
410
- function singular(w) {
411
- if (w.length <= 3)
412
- return w;
413
- if (/ies$/.test(w))
414
- return `${w.slice(0, -3)}y`;
415
- if (/(ss|us|is)$/.test(w))
416
- return w;
417
- if (/(sses|shes|ches|xes|zes)$/.test(w))
418
- return w.slice(0, -2);
419
- if (/ses$/.test(w))
420
- return w.slice(0, -1);
421
- if (/s$/.test(w))
422
- return w.slice(0, -1);
423
- return w;
424
- }
425
- var isNoise = (w) => STOP_TOKENS.has(w) || /^v\d+$/.test(w) || /^\d+$/.test(w);
426
- function businessWords(s) {
427
- return splitWords(s).filter((w) => !isNoise(w)).map(singular);
244
+ // packages/glossary/dist/wiki.js
245
+ var GLOSSARY = /\b(glossary|glossaries|definitions|terminology|terms|vocabulary)\b/i;
246
+ var META = /^(owner|owners|last updated|updated|status|version|author|date|classification|approver|approvers|reviewed by|reviewer|lead time|typical lead time|title|tags|see also|note|notes|example|examples|source|link|links|contact|team)$/i;
247
+ var strip = (s) => s.replace(/^\*\*(.+)\*\*$/, "$1").replace(/^__(.+)__$/, "$1").replace(/[`]/g, "").trim();
248
+ function isTermLike(raw) {
249
+ const t = strip(raw);
250
+ if (!t || t.length > 60 || !/^[A-Za-z]/.test(t))
251
+ return false;
252
+ if (/:\/\/|[|?!;]/.test(t) || /[.,]$/.test(t))
253
+ return false;
254
+ if (META.test(t))
255
+ return false;
256
+ const words2 = t.split(/\s+/);
257
+ return words2.length >= 1 && words2.length <= 5;
428
258
  }
429
- function compoundOf(name) {
430
- const w = splitWords(name).filter((x) => !/^\d+$/.test(x));
431
- while (w.length && (isNoise(w[0]) || QUALIFIERS.has(w[0])))
432
- w.shift();
433
- while (w.length && isNoise(w.at(-1)))
434
- w.pop();
435
- if (w.length < 2 || w.some(isNoise))
259
+ var cleanDefinition = (s) => s.replace(/\s+/g, " ").trim();
260
+ var BOLD = /^(?:\*\*([^*]+?)\*\*|__([^_]+?)__)\s*(?::|\s[—–-]\s)\s*(.+)$/;
261
+ var PLAIN = /^([A-Za-z][^:]{0,59}?)\s*(?::|\s[—–-]\s)\s*(.+)$/;
262
+ function lineDefinition(text2, scoped) {
263
+ const b = BOLD.exec(text2);
264
+ if (b) {
265
+ const term = (b[1] ?? b[2]).trim();
266
+ const definition = cleanDefinition(b[3]);
267
+ if (isTermLike(term) && definition.length >= 3)
268
+ return { term, definition, markup: "bold" };
436
269
  return void 0;
437
- return [...w.slice(0, -1), singular(w.at(-1))].join(" ");
438
- }
439
- function singleOf(name) {
440
- const w = businessWords(name).filter((x) => !QUALIFIERS.has(x));
441
- return w.length === 1 ? w[0] : void 0;
442
- }
443
- function termKey(term) {
444
- const w = term.toLowerCase().replace(/[`*_"'’]/g, "").split(/[^a-z0-9]+/).filter(Boolean);
445
- if (!w.length)
446
- return "";
447
- return [...w.slice(0, -1), singular(w.at(-1))].join(" ");
270
+ }
271
+ if (!scoped)
272
+ return void 0;
273
+ const p = PLAIN.exec(text2);
274
+ if (p && isTermLike(p[1])) {
275
+ const definition = cleanDefinition(p[2]);
276
+ if (definition.length >= 3)
277
+ return { term: strip(p[1]), definition, markup: "line" };
278
+ }
279
+ return void 0;
448
280
  }
449
- function termId(term) {
450
- return `term.${termKey(term).replace(/\s+/g, "-")}`.slice(0, 120);
281
+ var TERM_HEADER = /^(term|terms|name|word|phrase|concept)$/i;
282
+ var DEF_HEADER = /^(definition|definitions|meaning|description|what it means)$/i;
283
+ function scopes(page) {
284
+ const fmType = [page.frontMatter.type, page.frontMatter.kind].filter((v) => typeof v === "string").join(" ");
285
+ const pageWide = GLOSSARY.test(page.title) || GLOSSARY.test(fmType);
286
+ const out = [];
287
+ let openLevel;
288
+ for (const s of page.sections) {
289
+ if (openLevel !== void 0 && s.level > 0 && s.level <= openLevel)
290
+ openLevel = void 0;
291
+ if (openLevel === void 0 && s.level > 0 && GLOSSARY.test(s.heading))
292
+ openLevel = s.level;
293
+ out.push(pageWide || openLevel !== void 0);
294
+ }
295
+ return out;
451
296
  }
452
- function displayTerm(term) {
453
- const t = term.trim().replace(/\s+/g, " ");
454
- return t.charAt(0).toUpperCase() + t.slice(1);
297
+ function pageDefinitions(page) {
298
+ const out = [];
299
+ const add = (term, definition, line, markup) => {
300
+ const t = displayTerm(strip(term));
301
+ const key = termKey(t);
302
+ if (!key)
303
+ return;
304
+ out.push({
305
+ term: t,
306
+ key,
307
+ definition,
308
+ path: page.path,
309
+ line,
310
+ markup,
311
+ pageTitle: page.title,
312
+ ...page.owner ? { owner: page.owner } : {}
313
+ });
314
+ };
315
+ const inScope = scopes(page);
316
+ page.sections.forEach((s, si) => {
317
+ const scoped = inScope[si];
318
+ if (scoped && s.level >= 2 && !GLOSSARY.test(s.heading) && isTermLike(s.heading)) {
319
+ const first = s.blocks[0];
320
+ if (first?.type === "paragraph" && !lineDefinition(first.text.split("\n")[0], true))
321
+ add(s.heading, cleanDefinition(first.text), first.line, "heading");
322
+ }
323
+ for (const b of s.blocks) {
324
+ if (b.type === "paragraph") {
325
+ const lines = b.text.split("\n");
326
+ for (let k = 0; k < lines.length; k++) {
327
+ const l = lines[k];
328
+ const next = lines[k + 1];
329
+ if (next !== void 0 && /^:\s+\S/.test(next) && isTermLike(l)) {
330
+ add(l, cleanDefinition(next.replace(/^:\s+/, "")), b.line + k, "definition-list");
331
+ k++;
332
+ continue;
333
+ }
334
+ const d = lineDefinition(l, scoped);
335
+ if (d)
336
+ add(d.term, d.definition, b.line + k, d.markup);
337
+ }
338
+ } else if (b.type === "list") {
339
+ for (const it of b.items) {
340
+ const d = lineDefinition(it.text.split("\n")[0], scoped);
341
+ if (d)
342
+ add(d.term, cleanDefinition([d.definition, ...it.text.split("\n").slice(1)].join(" ")), it.line, d.markup === "line" ? "list" : d.markup);
343
+ }
344
+ } else if (b.type === "table" && scoped) {
345
+ const ti = b.header.findIndex((h) => TERM_HEADER.test(strip(h)));
346
+ const di = b.header.findIndex((h) => DEF_HEADER.test(strip(h)));
347
+ if (ti < 0 || di < 0)
348
+ continue;
349
+ for (const r of b.rows) {
350
+ const term = r.cells[ti] ?? "";
351
+ const def = cleanDefinition(r.cells[di] ?? "");
352
+ if (isTermLike(term) && def.length >= 3)
353
+ add(term, def, r.line, "table");
354
+ }
355
+ }
356
+ }
357
+ });
358
+ return out.sort((a, b) => a.line - b.line);
455
359
  }
456
- function definitionKey(d) {
457
- return d.toLowerCase().replace(/[^a-z0-9]+/g, " ").trim();
360
+ function wikiDefinitions(pages) {
361
+ return pages.flatMap(pageDefinitions).sort((a, b) => a.path === b.path ? a.line - b.line : a.path < b.path ? -1 : 1);
458
362
  }
459
363
 
460
- // packages/glossary/dist/decisions.js
461
- var GlossaryError = class extends Error {
462
- code;
463
- constructor(code, message) {
464
- super(message);
465
- this.code = code;
466
- this.name = "GlossaryError";
467
- }
468
- };
469
- var now = () => (/* @__PURE__ */ new Date()).toISOString().replace(/\.\d{3}Z$/, "Z");
470
- function decisionId(prefix, key) {
471
- const id = `${prefix}.${key}`;
472
- if (id.length <= 120)
473
- return id;
474
- return `${id.slice(0, 110)}-${createHash("sha256").update(id).digest("hex").slice(0, 8)}`;
475
- }
476
- function decision(id, target, action, value, reason, o) {
477
- if (!o.by.trim())
478
- throw new GlossaryError("invalid", "Say who decides (--by <handle>).");
479
- const p = parseRecord({
480
- schemaVersion: 2,
481
- kind: "CuratedDecision",
482
- id,
483
- status: "current",
484
- confidence: "asserted",
485
- evidence: [{ source: "manual", by: o.by }],
486
- target,
487
- action,
488
- value,
489
- reason: o.reason?.trim() || reason,
490
- by: o.by,
491
- at: o.at ?? now()
492
- });
493
- if (!p.ok)
494
- throw new GlossaryError("invalid", `That decision isn't valid: ${p.issues.map((i) => `${i.path}: ${i.message}`).join("; ")}`);
495
- return p.record;
496
- }
497
- function resolveTermId(termOrId) {
498
- const t = termOrId.trim();
499
- if (/^term\.[a-z0-9][a-z0-9._-]*$/.test(t))
500
- return t;
501
- const id = termId(t);
502
- if (id === "term.")
503
- throw new GlossaryError("invalid", `"${termOrId}" is not a term.`);
504
- return id;
505
- }
506
- function defineDecision(term, definition, o) {
507
- const def = definition.trim().replace(/\s+/g, " ");
508
- if (def.length < 3)
509
- throw new GlossaryError("invalid", "A definition needs a sentence.");
510
- const id = resolveTermId(term);
511
- const name = o.existing?.term ?? (term.startsWith("term.") ? void 0 : displayTerm(term));
512
- return decision(decisionId("glossary.define", id.slice("term.".length)), id, "assert", { ...name ? { term: name } : {}, definition: def, ...o.owner ? { owner: o.owner } : {} }, `Glossary definition of ${name ?? id}`, o);
513
- }
514
- function aliasDecision(term, aliases, o) {
515
- const id = resolveTermId(term);
516
- const byKey = /* @__PURE__ */ new Map();
517
- for (const a of aliases.map((x) => x.trim().replace(/\s+/g, " ")).filter(Boolean))
518
- if (!byKey.has(termKey(a)))
519
- byKey.set(termKey(a), a);
520
- const names = [...byKey.values()].sort();
521
- if (!names.length)
522
- throw new GlossaryError("invalid", "Give at least one other name.");
523
- return decision(decisionId("glossary.alias", id.slice("term.".length)), id, "alias", names, `Other names for ${id}`, o);
524
- }
525
- function linkDecision(term, recordId, records, o) {
526
- const byId = new Map(records.map((r) => [r.id, r]));
527
- const [base2, field] = recordId.split("#");
528
- const target = byId.get(base2);
529
- if (!target)
530
- throw new GlossaryError("not-found", `There is no record ${base2} in the catalog.`);
531
- if (field !== void 0) {
532
- if (target.kind !== "Schema" || !target.fields.some((f) => f.name === field))
533
- throw new GlossaryError("not-found", `${base2} has no schema field ${field}.`);
534
- } else if (target.kind === "Column") {
535
- return decision(decisionId("glossary.link", target.id), target.id, "assert", { glossaryTerm: term.id }, `${target.id} holds the glossary term ${term.term}`, o);
536
- }
537
- const fields = [.../* @__PURE__ */ new Set([...term.fields, recordId])].sort();
538
- return decision(decisionId("glossary.fields", term.id.slice("term.".length)), term.id, "assert", { fields }, `What the glossary term ${term.term} is linked to`, o);
539
- }
540
- async function writeDecisions(curatedDir, decisions) {
541
- const out = [];
542
- for (const d of decisions) {
543
- const rel = recordPath(d);
544
- const full = join2(curatedDir, ...rel.split("/"));
545
- await mkdir2(dirname(full), { recursive: true });
546
- await writeFile2(full, serializeRecord(d), "utf8");
547
- out.push(rel);
548
- }
549
- return out;
550
- }
551
-
552
- // packages/glossary/dist/scan.js
553
- import { mkdir as mkdir3, readdir as readdir2, readFile as readFile2, rm as rm2, writeFile as writeFile3 } from "node:fs/promises";
554
- import { dirname as dirname2, join as join3, relative, sep } from "node:path";
555
-
556
364
  // packages/glossary/dist/code.js
557
365
  var firstEvidence = (r) => r.evidence[0];
558
366
  function named(records) {
@@ -700,126 +508,6 @@ function fieldNames(records) {
700
508
  return named(records).filter((n) => n.isField).map((n) => ({ record: n.record, name: n.name }));
701
509
  }
702
510
 
703
- // packages/glossary/dist/wiki.js
704
- var GLOSSARY = /\b(glossary|glossaries|definitions|terminology|terms|vocabulary)\b/i;
705
- var META = /^(owner|owners|last updated|updated|status|version|author|date|classification|approver|approvers|reviewed by|reviewer|lead time|typical lead time|title|tags|see also|note|notes|example|examples|source|link|links|contact|team)$/i;
706
- var strip = (s) => s.replace(/^\*\*(.+)\*\*$/, "$1").replace(/^__(.+)__$/, "$1").replace(/[`]/g, "").trim();
707
- function isTermLike(raw) {
708
- const t = strip(raw);
709
- if (!t || t.length > 60 || !/^[A-Za-z]/.test(t))
710
- return false;
711
- if (/:\/\/|[|?!;]/.test(t) || /[.,]$/.test(t))
712
- return false;
713
- if (META.test(t))
714
- return false;
715
- const words2 = t.split(/\s+/);
716
- return words2.length >= 1 && words2.length <= 5;
717
- }
718
- var cleanDefinition = (s) => s.replace(/\s+/g, " ").trim();
719
- var BOLD = /^(?:\*\*([^*]+?)\*\*|__([^_]+?)__)\s*(?::|\s[—–-]\s)\s*(.+)$/;
720
- var PLAIN = /^([A-Za-z][^:]{0,59}?)\s*(?::|\s[—–-]\s)\s*(.+)$/;
721
- function lineDefinition(text, scoped) {
722
- const b = BOLD.exec(text);
723
- if (b) {
724
- const term = (b[1] ?? b[2]).trim();
725
- const definition = cleanDefinition(b[3]);
726
- if (isTermLike(term) && definition.length >= 3)
727
- return { term, definition, markup: "bold" };
728
- return void 0;
729
- }
730
- if (!scoped)
731
- return void 0;
732
- const p = PLAIN.exec(text);
733
- if (p && isTermLike(p[1])) {
734
- const definition = cleanDefinition(p[2]);
735
- if (definition.length >= 3)
736
- return { term: strip(p[1]), definition, markup: "line" };
737
- }
738
- return void 0;
739
- }
740
- var TERM_HEADER = /^(term|terms|name|word|phrase|concept)$/i;
741
- var DEF_HEADER = /^(definition|definitions|meaning|description|what it means)$/i;
742
- function scopes(page) {
743
- const fmType = [page.frontMatter.type, page.frontMatter.kind].filter((v) => typeof v === "string").join(" ");
744
- const pageWide = GLOSSARY.test(page.title) || GLOSSARY.test(fmType);
745
- const out = [];
746
- let openLevel;
747
- for (const s of page.sections) {
748
- if (openLevel !== void 0 && s.level > 0 && s.level <= openLevel)
749
- openLevel = void 0;
750
- if (openLevel === void 0 && s.level > 0 && GLOSSARY.test(s.heading))
751
- openLevel = s.level;
752
- out.push(pageWide || openLevel !== void 0);
753
- }
754
- return out;
755
- }
756
- function pageDefinitions(page) {
757
- const out = [];
758
- const add = (term, definition, line, markup) => {
759
- const t = displayTerm(strip(term));
760
- const key = termKey(t);
761
- if (!key)
762
- return;
763
- out.push({
764
- term: t,
765
- key,
766
- definition,
767
- path: page.path,
768
- line,
769
- markup,
770
- pageTitle: page.title,
771
- ...page.owner ? { owner: page.owner } : {}
772
- });
773
- };
774
- const inScope = scopes(page);
775
- page.sections.forEach((s, si) => {
776
- const scoped = inScope[si];
777
- if (scoped && s.level >= 2 && !GLOSSARY.test(s.heading) && isTermLike(s.heading)) {
778
- const first = s.blocks[0];
779
- if (first?.type === "paragraph" && !lineDefinition(first.text.split("\n")[0], true))
780
- add(s.heading, cleanDefinition(first.text), first.line, "heading");
781
- }
782
- for (const b of s.blocks) {
783
- if (b.type === "paragraph") {
784
- const lines = b.text.split("\n");
785
- for (let k = 0; k < lines.length; k++) {
786
- const l = lines[k];
787
- const next = lines[k + 1];
788
- if (next !== void 0 && /^:\s+\S/.test(next) && isTermLike(l)) {
789
- add(l, cleanDefinition(next.replace(/^:\s+/, "")), b.line + k, "definition-list");
790
- k++;
791
- continue;
792
- }
793
- const d = lineDefinition(l, scoped);
794
- if (d)
795
- add(d.term, d.definition, b.line + k, d.markup);
796
- }
797
- } else if (b.type === "list") {
798
- for (const it of b.items) {
799
- const d = lineDefinition(it.text.split("\n")[0], scoped);
800
- if (d)
801
- add(d.term, cleanDefinition([d.definition, ...it.text.split("\n").slice(1)].join(" ")), it.line, d.markup === "line" ? "list" : d.markup);
802
- }
803
- } else if (b.type === "table" && scoped) {
804
- const ti = b.header.findIndex((h) => TERM_HEADER.test(strip(h)));
805
- const di = b.header.findIndex((h) => DEF_HEADER.test(strip(h)));
806
- if (ti < 0 || di < 0)
807
- continue;
808
- for (const r of b.rows) {
809
- const term = r.cells[ti] ?? "";
810
- const def = cleanDefinition(r.cells[di] ?? "");
811
- if (isTermLike(term) && def.length >= 3)
812
- add(term, def, r.line, "table");
813
- }
814
- }
815
- }
816
- });
817
- return out.sort((a, b) => a.line - b.line);
818
- }
819
- function wikiDefinitions(pages) {
820
- return pages.flatMap(pageDefinitions).sort((a, b) => a.path === b.path ? a.line - b.line : a.path < b.path ? -1 : 1);
821
- }
822
-
823
511
  // packages/glossary/dist/build.js
824
512
  var isDecision = (r) => r.kind === "CuratedDecision" && r.status === "current";
825
513
  var isObj = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
@@ -1021,38 +709,377 @@ function buildGlossary(input) {
1021
709
  for (const col of linkedColumns)
1022
710
  columnLinks.push({ column: col, term: id, how: "decision" });
1023
711
  }
1024
- const decided = new Set(columnLinks.filter((l) => l.how === "decision").map((l) => l.column));
1025
- const links = columnLinks.filter((l) => l.how === "decision" || !decided.has(l.column)).sort((a, b) => a.column.localeCompare(b.column) || a.term.localeCompare(b.term));
1026
- const covered = /* @__PURE__ */ new Set([...terms.flatMap((t) => t.fields), ...links.map((l) => l.column)]);
1027
- const names = entries.map((e) => ({ id: e.id, term: e.term, aliases: e.aliases }));
1028
- const hints = fieldNames(records).filter((f) => !covered.has(f.record)).map((f) => namingHint(f.record, f.name, names)).filter((h) => !!h);
1029
- return {
1030
- terms,
1031
- entries,
1032
- columnLinks: links,
1033
- conflicts: conflicts.sort((a, b) => a.term.localeCompare(b.term) || a.kind.localeCompare(b.kind)),
1034
- hints,
1035
- problems
1036
- };
712
+ const decided = new Set(columnLinks.filter((l) => l.how === "decision").map((l) => l.column));
713
+ const links = columnLinks.filter((l) => l.how === "decision" || !decided.has(l.column)).sort((a, b) => a.column.localeCompare(b.column) || a.term.localeCompare(b.term));
714
+ const covered = /* @__PURE__ */ new Set([...terms.flatMap((t) => t.fields), ...links.map((l) => l.column)]);
715
+ const names = entries.map((e) => ({ id: e.id, term: e.term, aliases: e.aliases }));
716
+ const hints = fieldNames(records).filter((f) => !covered.has(f.record)).map((f) => namingHint(f.record, f.name, names)).filter((h) => !!h);
717
+ return {
718
+ terms,
719
+ entries,
720
+ columnLinks: links,
721
+ conflicts: conflicts.sort((a, b) => a.term.localeCompare(b.term) || a.kind.localeCompare(b.kind)),
722
+ hints,
723
+ problems
724
+ };
725
+ }
726
+ function applyGlossary(records, build) {
727
+ const link = /* @__PURE__ */ new Map();
728
+ for (const l of build.columnLinks)
729
+ if (!link.has(l.column))
730
+ link.set(l.column, l.term);
731
+ const out = [];
732
+ for (const r of records) {
733
+ if (r.kind === "GlossaryTerm")
734
+ continue;
735
+ if (r.kind === "Column" && !r.glossaryTerm && link.has(r.id))
736
+ out.push({ ...r, glossaryTerm: link.get(r.id) });
737
+ else
738
+ out.push(r);
739
+ }
740
+ return [...out, ...build.terms];
741
+ }
742
+
743
+ // packages/glossary/dist/proposals.js
744
+ var import_yaml = __toESM(require_dist(), 1);
745
+ import { mkdir, readdir, readFile, rm, writeFile } from "node:fs/promises";
746
+ import { join } from "node:path";
747
+ var GLOSSARY_DEFINER = "glossary-definer";
748
+ var GLOSSARY_CHANGE = "glossary";
749
+ var GLOSSARY_OUTPUT = "catalog/proposals/glossary-definitions.yaml";
750
+ var DefinitionProposalFile = external_exports.strictObject({
751
+ schemaVersion: external_exports.literal(1),
752
+ term: RecordId,
753
+ definition: external_exports.string().min(1),
754
+ evidence: external_exports.array(external_exports.string().min(1)).min(1),
755
+ agent: RecordId,
756
+ runId: external_exports.string().optional(),
757
+ at: IsoDateTime
758
+ });
759
+ var LEADING = ["schemaVersion", "term", "definition", "evidence"];
760
+ async function writeDefinitionProposals(dir, proposals) {
761
+ const written = [];
762
+ if (!proposals.length)
763
+ return written;
764
+ await mkdir(join(dir, "glossaryterm"), { recursive: true });
765
+ for (const p of proposals) {
766
+ const file = `glossaryterm/${p.term}.yaml`;
767
+ const body = DefinitionProposalFile.parse({ schemaVersion: 1, ...p });
768
+ await writeFile(join(dir, file), toCanonicalYaml(body, LEADING), "utf8");
769
+ written.push(file);
770
+ }
771
+ return written;
772
+ }
773
+ async function readDefinitionProposals(dir) {
774
+ const proposals = [];
775
+ const problems = [];
776
+ let files;
777
+ try {
778
+ files = (await readdir(join(dir, "glossaryterm"))).filter((f) => /\.ya?ml$/.test(f)).sort();
779
+ } catch {
780
+ return { proposals, problems };
781
+ }
782
+ for (const f of files) {
783
+ const file = `glossaryterm/${f}`;
784
+ let data;
785
+ try {
786
+ data = (0, import_yaml.parse)(await readFile(join(dir, file), "utf8"));
787
+ } catch (e) {
788
+ problems.push({ file, message: `invalid YAML: ${e.message.split("\n")[0]}` });
789
+ continue;
790
+ }
791
+ const p = DefinitionProposalFile.safeParse(data);
792
+ if (!p.success) {
793
+ problems.push({
794
+ file,
795
+ message: p.error.issues.map((i) => `${i.path.join(".")}: ${i.message}`).join("; ")
796
+ });
797
+ continue;
798
+ }
799
+ const { schemaVersion: _v, ...rest } = p.data;
800
+ void _v;
801
+ proposals.push(rest);
802
+ }
803
+ return { proposals, problems };
804
+ }
805
+ function glossaryDefinerAgent(orgPack) {
806
+ const own = orgPack?.agents.find((a) => a.id === GLOSSARY_DEFINER);
807
+ if (own)
808
+ return own;
809
+ return AgentDefinition.parse({
810
+ schemaVersion: 1,
811
+ id: GLOSSARY_DEFINER,
812
+ role: "business-analyst",
813
+ description: "Proposes a one-sentence business definition for each glossary term found only in code, from where the code uses it.",
814
+ inputs: ["glossary", "catalog-slice"],
815
+ // C5 has no output kind for catalog proposals (see ADR 0031); a definition seeds specs.
816
+ outputArtifact: "specs",
817
+ tokenBudget: orgPack?.runtime.budgets.defaultTokens ?? 15e4,
818
+ allowedRuntimes: ["hand-off", "model", "headless-claude-code", "mock", "replay"]
819
+ });
820
+ }
821
+ var INSTRUCTIONS = `# Glossary definer
822
+
823
+ You propose business definitions for words an estate's code uses that no wiki page defines. Each term below is a data block: its id, the word, and the records (endpoints, topics, tables, columns, schema fields) that use it. For each term propose:
824
+
825
+ - definition: one sentence a product owner would accept, saying what the word means to the business, only from the evidence;
826
+ - evidence: the record ids from that term's block your definition relies on.
827
+
828
+ Don't invent systems, rules or numbers. A proposal that cites no record id from its term is discarded. Write YAML and nothing else:
829
+
830
+ terms:
831
+ - id: <term id>
832
+ definition: <one sentence>
833
+ evidence: [<record id>, ...]`;
834
+ var Proposal = external_exports.object({
835
+ id: external_exports.string().min(1),
836
+ definition: external_exports.string().trim().min(3).max(400),
837
+ evidence: external_exports.array(external_exports.coerce.string()).default([])
838
+ });
839
+ function block(e) {
840
+ return toCanonicalYaml({
841
+ id: e.id,
842
+ term: e.term,
843
+ usedBy: (e.code?.uses ?? []).map((u) => ({
844
+ id: u.record,
845
+ kind: u.kind,
846
+ name: u.name,
847
+ ...u.evidence.path ? {
848
+ code: `${u.evidence.repo ? `${u.evidence.repo}:` : ""}${u.evidence.path}${u.evidence.line ? `:${u.evidence.line}` : ""}`
849
+ } : {}
850
+ }))
851
+ }, ["id", "term", "usedBy", "id", "kind", "name", "code"]).trimEnd();
852
+ }
853
+ function parseDefinerOutput(output, entries, meta) {
854
+ let data;
855
+ try {
856
+ data = (0, import_yaml.parse)(output.replace(/^```(?:ya?ml)?\s*\n|\n```\s*$/g, ""));
857
+ } catch (e) {
858
+ return {
859
+ proposals: [],
860
+ discarded: [{ reason: `the output is not YAML: ${e.message.split("\n")[0]}` }]
861
+ };
862
+ }
863
+ const list = data?.terms;
864
+ if (!Array.isArray(list))
865
+ return { proposals: [], discarded: [{ reason: "the output has no terms list" }] };
866
+ const byId = new Map(entries.map((e) => [e.id, e]));
867
+ const out = { proposals: [], discarded: [] };
868
+ for (const raw of list) {
869
+ const p = Proposal.safeParse(raw);
870
+ const id = typeof raw?.id === "string" ? raw.id : void 0;
871
+ if (!p.success) {
872
+ out.discarded.push({ ...id ? { term: id } : {}, reason: "incomplete proposal" });
873
+ continue;
874
+ }
875
+ const e = byId.get(p.data.id);
876
+ if (!e) {
877
+ out.discarded.push({
878
+ term: p.data.id,
879
+ reason: `${p.data.id} isn't one of the terms it was given`
880
+ });
881
+ continue;
882
+ }
883
+ if (out.proposals.some((x) => x.term === e.id)) {
884
+ out.discarded.push({ term: e.id, reason: "a second proposal for the same term" });
885
+ continue;
886
+ }
887
+ const known = new Set((e.code?.uses ?? []).map((u) => u.record));
888
+ const evidence = [...new Set(p.data.evidence.filter((x) => known.has(x)))];
889
+ if (!evidence.length) {
890
+ out.discarded.push({ term: e.id, reason: "it cites no record that uses the term" });
891
+ continue;
892
+ }
893
+ out.proposals.push({
894
+ term: e.id,
895
+ definition: p.data.definition.replace(/\s+/g, " "),
896
+ evidence,
897
+ agent: meta.agent,
898
+ at: meta.at,
899
+ ...meta.runId ? { runId: meta.runId } : {}
900
+ });
901
+ }
902
+ return out;
903
+ }
904
+ function undefinedCodeTerms(entries) {
905
+ return entries.filter((e) => e.sources.length === 1 && e.sources[0] === "code" && (e.code?.uses.length ?? 0) > 0);
906
+ }
907
+ async function proposeDefinitions(entries, runtime, o = {}) {
908
+ const todo = [...entries].sort((a, b) => a.id.localeCompare(b.id));
909
+ if (!todo.length)
910
+ return { proposals: [], discarded: [] };
911
+ const agent = glossaryDefinerAgent(o.orgPack);
912
+ const now2 = o.now ?? /* @__PURE__ */ new Date();
913
+ const at3 = now2.toISOString().replace(/\.\d{3}Z$/, "Z");
914
+ const levels = o.orgPack?.pack?.classification ?? [
915
+ "public",
916
+ "internal",
917
+ "confidential",
918
+ "restricted"
919
+ ];
920
+ const sources = [
921
+ {
922
+ id: "0-instructions",
923
+ kind: "instruction",
924
+ origin: "atticus/glossary",
925
+ text: INSTRUCTIONS,
926
+ classification: "public",
927
+ trusted: true
928
+ },
929
+ ...todo.map((e) => ({
930
+ id: e.id,
931
+ kind: "catalog",
932
+ origin: `catalog/generated/glossaryterm/${e.id}.yaml`,
933
+ text: block(e),
934
+ classification: "internal",
935
+ priority: 10
936
+ }))
937
+ ];
938
+ const pack = buildContextPack({
939
+ change: GLOSSARY_CHANGE,
940
+ agent: agent.id,
941
+ budget: o.orgPack?.runtime.budgets.perAgent[agent.id] ?? agent.tokenBudget,
942
+ sources,
943
+ levels,
944
+ maxClassification: levels.includes("internal") ? "internal" : levels.at(-1),
945
+ now: now2
946
+ });
947
+ const sent = todo.filter((e) => pack.manifest.sources.some((s) => s.id === e.id));
948
+ const discarded = todo.filter((e) => !sent.includes(e)).map((e) => ({
949
+ term: e.id,
950
+ reason: "left out of the context pack (budget or classification)"
951
+ }));
952
+ const prepared = await runtime.prepare({
953
+ change: GLOSSARY_CHANGE,
954
+ agent,
955
+ pack,
956
+ outputPath: GLOSSARY_OUTPUT
957
+ });
958
+ const run = await runtime.run(prepared.runId);
959
+ if (run.state === "waiting" || run.state === "prepared")
960
+ return {
961
+ proposals: [],
962
+ discarded,
963
+ waiting: run.runId,
964
+ promptSha256: pack.manifest.promptSha256
965
+ };
966
+ if (run.state !== "validated" || run.output === void 0)
967
+ return {
968
+ proposals: [],
969
+ discarded: [
970
+ ...discarded,
971
+ { reason: `the glossary definer ${run.state}: ${run.errors.join("; ") || "no output"}` }
972
+ ],
973
+ run: toRunSummary(run),
974
+ promptSha256: pack.manifest.promptSha256
975
+ };
976
+ const parsed = parseDefinerOutput(run.output, sent, { agent: agent.id, at: at3, runId: run.runId });
977
+ return {
978
+ proposals: parsed.proposals,
979
+ discarded: [...discarded, ...parsed.discarded],
980
+ run: toRunSummary(run),
981
+ promptSha256: pack.manifest.promptSha256
982
+ };
983
+ }
984
+
985
+ // packages/glossary/dist/decisions.js
986
+ import { createHash } from "node:crypto";
987
+ import { mkdir as mkdir2, writeFile as writeFile2 } from "node:fs/promises";
988
+ import { dirname, join as join2 } from "node:path";
989
+ var GlossaryError = class extends Error {
990
+ code;
991
+ constructor(code, message) {
992
+ super(message);
993
+ this.code = code;
994
+ this.name = "GlossaryError";
995
+ }
996
+ };
997
+ var now = () => (/* @__PURE__ */ new Date()).toISOString().replace(/\.\d{3}Z$/, "Z");
998
+ function decisionId(prefix, key) {
999
+ const id = `${prefix}.${key}`;
1000
+ if (id.length <= 120)
1001
+ return id;
1002
+ return `${id.slice(0, 110)}-${createHash("sha256").update(id).digest("hex").slice(0, 8)}`;
1003
+ }
1004
+ function decision(id, target, action, value, reason, o) {
1005
+ if (!o.by.trim())
1006
+ throw new GlossaryError("invalid", "Say who decides (--by <handle>).");
1007
+ const p = parseRecord({
1008
+ schemaVersion: 2,
1009
+ kind: "CuratedDecision",
1010
+ id,
1011
+ status: "current",
1012
+ confidence: "asserted",
1013
+ evidence: [{ source: "manual", by: o.by }],
1014
+ target,
1015
+ action,
1016
+ value,
1017
+ reason: o.reason?.trim() || reason,
1018
+ by: o.by,
1019
+ at: o.at ?? now()
1020
+ });
1021
+ if (!p.ok)
1022
+ throw new GlossaryError("invalid", `That decision isn't valid: ${p.issues.map((i) => `${i.path}: ${i.message}`).join("; ")}`);
1023
+ return p.record;
1024
+ }
1025
+ function resolveTermId(termOrId) {
1026
+ const t = termOrId.trim();
1027
+ if (/^term\.[a-z0-9][a-z0-9._-]*$/.test(t))
1028
+ return t;
1029
+ const id = termId(t);
1030
+ if (id === "term.")
1031
+ throw new GlossaryError("invalid", `"${termOrId}" is not a term.`);
1032
+ return id;
1033
+ }
1034
+ function defineDecision(term, definition, o) {
1035
+ const def = definition.trim().replace(/\s+/g, " ");
1036
+ if (def.length < 3)
1037
+ throw new GlossaryError("invalid", "A definition needs a sentence.");
1038
+ const id = resolveTermId(term);
1039
+ const name = o.existing?.term ?? (term.startsWith("term.") ? void 0 : displayTerm(term));
1040
+ return decision(decisionId("glossary.define", id.slice("term.".length)), id, "assert", { ...name ? { term: name } : {}, definition: def, ...o.owner ? { owner: o.owner } : {} }, `Glossary definition of ${name ?? id}`, o);
1041
+ }
1042
+ function aliasDecision(term, aliases, o) {
1043
+ const id = resolveTermId(term);
1044
+ const byKey = /* @__PURE__ */ new Map();
1045
+ for (const a of aliases.map((x) => x.trim().replace(/\s+/g, " ")).filter(Boolean))
1046
+ if (!byKey.has(termKey(a)))
1047
+ byKey.set(termKey(a), a);
1048
+ const names = [...byKey.values()].sort();
1049
+ if (!names.length)
1050
+ throw new GlossaryError("invalid", "Give at least one other name.");
1051
+ return decision(decisionId("glossary.alias", id.slice("term.".length)), id, "alias", names, `Other names for ${id}`, o);
1052
+ }
1053
+ function linkDecision(term, recordId, records, o) {
1054
+ const byId = new Map(records.map((r) => [r.id, r]));
1055
+ const [base2, field] = recordId.split("#");
1056
+ const target = byId.get(base2);
1057
+ if (!target)
1058
+ throw new GlossaryError("not-found", `There is no record ${base2} in the catalog.`);
1059
+ if (field !== void 0) {
1060
+ if (target.kind !== "Schema" || !target.fields.some((f) => f.name === field))
1061
+ throw new GlossaryError("not-found", `${base2} has no schema field ${field}.`);
1062
+ } else if (target.kind === "Column") {
1063
+ return decision(decisionId("glossary.link", target.id), target.id, "assert", { glossaryTerm: term.id }, `${target.id} holds the glossary term ${term.term}`, o);
1064
+ }
1065
+ const fields = [.../* @__PURE__ */ new Set([...term.fields, recordId])].sort();
1066
+ return decision(decisionId("glossary.fields", term.id.slice("term.".length)), term.id, "assert", { fields }, `What the glossary term ${term.term} is linked to`, o);
1037
1067
  }
1038
- function applyGlossary(records, build) {
1039
- const link = /* @__PURE__ */ new Map();
1040
- for (const l of build.columnLinks)
1041
- if (!link.has(l.column))
1042
- link.set(l.column, l.term);
1068
+ async function writeDecisions(curatedDir, decisions) {
1043
1069
  const out = [];
1044
- for (const r of records) {
1045
- if (r.kind === "GlossaryTerm")
1046
- continue;
1047
- if (r.kind === "Column" && !r.glossaryTerm && link.has(r.id))
1048
- out.push({ ...r, glossaryTerm: link.get(r.id) });
1049
- else
1050
- out.push(r);
1070
+ for (const d of decisions) {
1071
+ const rel = recordPath(d);
1072
+ const full = join2(curatedDir, ...rel.split("/"));
1073
+ await mkdir2(dirname(full), { recursive: true });
1074
+ await writeFile2(full, serializeRecord(d), "utf8");
1075
+ out.push(rel);
1051
1076
  }
1052
- return [...out, ...build.terms];
1077
+ return out;
1053
1078
  }
1054
1079
 
1055
1080
  // packages/glossary/dist/scan.js
1081
+ import { mkdir as mkdir3, readdir as readdir2, readFile as readFile2, rm as rm2, writeFile as writeFile3 } from "node:fs/promises";
1082
+ import { dirname as dirname2, join as join3, relative, sep } from "node:path";
1056
1083
  async function readRecords(dir) {
1057
1084
  const records = [];
1058
1085
  const problems = [];
@@ -1174,16 +1201,16 @@ async function scanGlossary(root, o = {}) {
1174
1201
  }
1175
1202
  }
1176
1203
  const before = new Map(g.base.map((r) => [`${r.kind}/${r.id}`, r]));
1177
- const put = async (rel, text) => {
1204
+ const put = async (rel, text2) => {
1178
1205
  const full = join3(generated, ...rel.split("/"));
1179
1206
  await mkdir3(dirname2(full), { recursive: true });
1180
- await writeFile3(full, text, "utf8");
1207
+ await writeFile3(full, text2, "utf8");
1181
1208
  written.push(rel);
1182
1209
  };
1183
- for (const [rel, text] of want) {
1210
+ for (const [rel, text2] of want) {
1184
1211
  const cur = await readFile2(join3(generated, ...rel.split("/")), "utf8").catch(() => void 0);
1185
- if (cur !== text)
1186
- await put(rel, text);
1212
+ if (cur !== text2)
1213
+ await put(rel, text2);
1187
1214
  }
1188
1215
  const termIds = new Set(g.terms.map((t) => t.id));
1189
1216
  for (const r of g.records) {
@@ -1221,6 +1248,49 @@ async function completeGlossaryDefinitions(root, run, o = {}) {
1221
1248
  return { ...scanned, propose };
1222
1249
  }
1223
1250
 
1251
+ // packages/scanner/dist/federate-loose.js
1252
+ function resolveFederatedLooseEnds(o) {
1253
+ const originOf = (r) => (r.flags ?? []).find((f) => f.startsWith("federated:"))?.slice("federated:".length);
1254
+ const loose = o.federated.filter((r) => r.kind === "Unresolved");
1255
+ if (!loose.length)
1256
+ return { federated: o.federated, resolved: /* @__PURE__ */ new Map() };
1257
+ const fedComponents = o.federated.filter((r) => r.kind === "Component");
1258
+ const result = o.merge([...o.raw, ...fedComponents, ...loose]);
1259
+ const have = new Set(o.local.map((r) => `${r.kind}/${r.id}`));
1260
+ const known = /* @__PURE__ */ new Set([
1261
+ ...o.local.filter((r) => r.kind === "Component" || r.kind === "System").map((r) => r.id),
1262
+ ...fedComponents.map((r) => r.id)
1263
+ ]);
1264
+ const origin = new Map(fedComponents.map((c) => [c.id, originOf(c)]));
1265
+ const stillLoose = new Set(result.records.filter((r) => r.kind === "Unresolved").map((r) => r.id));
1266
+ const added = [];
1267
+ for (const r of result.records) {
1268
+ if (r.kind !== "Interface" || have.has(`Interface/${r.id}`))
1269
+ continue;
1270
+ const i = r;
1271
+ const from = origin.get(i.from) ?? origin.get(i.to);
1272
+ if (!from || !known.has(i.from) || !known.has(i.to))
1273
+ continue;
1274
+ added.push({ ...r, flags: [.../* @__PURE__ */ new Set([...r.flags ?? [], `federated:${from}`])] });
1275
+ }
1276
+ const key = (e) => JSON.stringify(e);
1277
+ const carried = new Set(added.flatMap((i) => i.evidence.map(key)));
1278
+ const resolved = /* @__PURE__ */ new Map();
1279
+ const kept2 = o.federated.filter((r) => {
1280
+ if (r.kind !== "Unresolved" || stillLoose.has(r.id))
1281
+ return true;
1282
+ if (!r.evidence.some((e) => e.source !== "export" && carried.has(key(e))))
1283
+ return true;
1284
+ const id = originOf(r) ?? "";
1285
+ resolved.set(id, (resolved.get(id) ?? 0) + 1);
1286
+ return false;
1287
+ });
1288
+ return {
1289
+ federated: [...kept2, ...added].sort((a, b) => a.kind === b.kind ? cmpText(a.id, b.id) : cmpText(a.kind, b.kind)),
1290
+ resolved
1291
+ };
1292
+ }
1293
+
1224
1294
  // packages/scanner/dist/files.js
1225
1295
  import { readdir as readdir3, readFile as readFile3, rm as rm3 } from "node:fs/promises";
1226
1296
  import { join as join4, relative as relative2, resolve, sep as sep2 } from "node:path";
@@ -1259,7 +1329,7 @@ async function readCatalogFiles(dir, cacheRoot) {
1259
1329
  } catch {
1260
1330
  return;
1261
1331
  }
1262
- for (const e of entries.sort((a, b) => a.name.localeCompare(b.name))) {
1332
+ for (const e of entries.sort((a, b) => cmpText(a.name, b.name))) {
1263
1333
  const full = join4(d, e.name);
1264
1334
  if (e.isDirectory())
1265
1335
  await walk(full);
@@ -1274,8 +1344,8 @@ async function readCatalogFiles(dir, cacheRoot) {
1274
1344
  name: "catalog",
1275
1345
  version: CATALOG_CACHE_VERSION,
1276
1346
  cacheRoot,
1277
- parse: (text) => {
1278
- const r = deserializeRecord(text);
1347
+ parse: (text2) => {
1348
+ const r = deserializeRecord(text2);
1279
1349
  return r.ok ? { ok: true, value: r.record } : { ok: false, message: r.issues.map((i) => `${i.path}: ${i.message}`).join("; ") };
1280
1350
  }
1281
1351
  })) {
@@ -1284,7 +1354,7 @@ async function readCatalogFiles(dir, cacheRoot) {
1284
1354
  else
1285
1355
  problems.push({ file: p.rel, message: p.problem });
1286
1356
  }
1287
- records.sort((a, b) => a.kind === b.kind ? a.id.localeCompare(b.id) : a.kind.localeCompare(b.kind));
1357
+ records.sort((a, b) => a.kind === b.kind ? cmpText(a.id, b.id) : cmpText(a.kind, b.kind));
1288
1358
  return { records, problems };
1289
1359
  }
1290
1360
  async function writeCatalog(dir, records) {
@@ -1292,14 +1362,14 @@ async function writeCatalog(dir, records) {
1292
1362
  const wanted = new Map(records.map((r) => [recordPath(r), serializeRecord(r)]));
1293
1363
  let written = 0;
1294
1364
  let unchanged = 0;
1295
- for (const [rel, text] of wanted) {
1365
+ for (const [rel, text2] of wanted) {
1296
1366
  const full = join4(dir, ...rel.split("/"));
1297
1367
  const current = await readFile3(full, "utf8").catch(() => void 0);
1298
- if (current === text) {
1368
+ if (current?.replace(/\r\n?/g, "\n") === text2) {
1299
1369
  unchanged++;
1300
1370
  continue;
1301
1371
  }
1302
- await writeFileAtomic(full, text);
1372
+ await writeFileAtomic(full, text2);
1303
1373
  written++;
1304
1374
  }
1305
1375
  let removed = 0;
@@ -1355,7 +1425,7 @@ function diffCatalogs(before, after) {
1355
1425
  for (const [k, r] of a)
1356
1426
  if (!b.has(k))
1357
1427
  diff.removed.push({ kind: r.kind, id: r.id });
1358
- const sort = (xs) => xs.sort((x, y) => x.kind === y.kind ? x.id.localeCompare(y.id) : x.kind.localeCompare(y.kind));
1428
+ const sort = (xs) => xs.sort((x, y) => x.kind === y.kind ? cmpText(x.id, y.id) : cmpText(x.kind, y.kind));
1359
1429
  sort(diff.added);
1360
1430
  sort(diff.removed);
1361
1431
  sort(diff.changed);
@@ -1380,16 +1450,19 @@ function prototypeScanNotes(op, only) {
1380
1450
  function isPrototypeRepo(op, repo) {
1381
1451
  return estateRepos(op).some((r) => r.id === repo && isPrototypeType(r.type));
1382
1452
  }
1383
- function estateMergeInput(op, repos, records, modulesOf = (id) => repos.find((r) => r.id === id)?.modules ?? []) {
1453
+ function estateMergeInput(op, repos, records, modulesOf = (id) => repos.find((r) => r.id === id)?.modules ?? [], read) {
1384
1454
  return {
1385
1455
  records,
1456
+ // ADR 0180: the organisation's component types, so a cited type is never demoted in a merge.
1457
+ componentTypes: new Set(effectiveComponentTypes(op.componentTypes).map((t) => t.id)),
1386
1458
  repos: repos.flatMap((r) => [
1387
1459
  {
1388
1460
  id: r.id,
1389
1461
  type: r.type,
1390
1462
  ...r.component ? { component: r.component } : {},
1391
1463
  ...r.system ? { system: r.system } : {},
1392
- hosts: r.hosts
1464
+ hosts: r.hosts,
1465
+ ...read?.has(r.id) ? { read: true } : {}
1393
1466
  },
1394
1467
  // A module's hosts join calls to its own component (ADR 0057).
1395
1468
  ...modulesOf(r.id).filter((m) => m.hosts.length > 0).map((m) => ({
@@ -1405,17 +1478,348 @@ function estateMergeInput(op, repos, records, modulesOf = (id) => repos.find((r)
1405
1478
  profilePath: op.profiles[0] ? `org-pack/profiles/${op.profiles[0].platform}/profile.yaml` : void 0
1406
1479
  };
1407
1480
  }
1408
- function mergeEstate(op, repos, records, modulesOf) {
1409
- return mergeCatalog(estateMergeInput(op, repos, records, modulesOf));
1481
+ function mergeEstate(op, repos, records, modulesOf, read) {
1482
+ return mergeCatalog(estateMergeInput(op, repos, records, modulesOf, read));
1410
1483
  }
1411
1484
  function curatedDirOf(root, op) {
1412
1485
  return join5(root, ...op.sources.layout.catalog.split("/"), "curated");
1413
1486
  }
1414
1487
 
1488
+ // packages/scanner/dist/facts-scan.js
1489
+ import { readFile as readFile4 } from "node:fs/promises";
1490
+ import { join as join6 } from "node:path";
1491
+
1492
+ // packages/scanner/dist/facts-catalog.js
1493
+ import { createHash as createHash2 } from "node:crypto";
1494
+
1495
+ // packages/scanner/dist/target-suggest.js
1496
+ var TARGETS_MAX_SUGGESTED = 5;
1497
+ var TARGETS_ANOTHER = TARGETS_ANOTHER_LABEL;
1498
+ var TARGETS_NO_EVIDENCE = "no evidence; listed so you can pick it";
1499
+ var STOP = new Set("url uri base host client clients service services svc api apis http https rest endpoint endpoints impl java kt ts js py cs go src main test get post put delete patch the and for com www internal example config configs properties yaml yml env app apps application new call calls feign template web request requests response gateway proxy remote adapter port".split(" "));
1500
+ function nameWords(s) {
1501
+ if (!s)
1502
+ return [];
1503
+ return [
1504
+ ...new Set(s.replace(/([a-z0-9])([A-Z])/g, "$1 $2").toLowerCase().split(/[^a-z0-9]+/).filter((w) => w.length >= 3 && !STOP.has(w) && !/^v?\d+$/.test(w)).map((w) => w.length > 4 && w.endsWith("s") && !w.endsWith("ss") ? w.slice(0, -1) : w))
1505
+ ];
1506
+ }
1507
+ var segs = (p) => p.split(/[?#]/)[0].split("/").filter(Boolean).map((x) => /^[{:<$]/.test(x) ? "{}" : x);
1508
+ var sameSeg = (a, b) => a === "{}" || b === "{}" || a === b;
1509
+ var under = (path, prefix) => {
1510
+ const a = segs(prefix);
1511
+ const b = segs(path);
1512
+ return a.length > 0 && a.length <= b.length && a.every((x, i) => x === b[i]);
1513
+ };
1514
+ function serves(entry, call, bases) {
1515
+ const e = segs(entry);
1516
+ const tries = [
1517
+ segs(call),
1518
+ ...bases.filter((b) => under(call, b)).map((b) => segs(call).slice(segs(b).length))
1519
+ ];
1520
+ return tries.some((c) => c.length === e.length && c.every((x, i) => sameSeg(x, e[i])));
1521
+ }
1522
+ function clientOf(f) {
1523
+ const q = f.cite.quote ?? "";
1524
+ const named2 = /\b([A-Z][A-Za-z0-9]*(?:Client|Service|Api|Feign|Gateway|Proxy))\b/.exec(q)?.[1];
1525
+ if (named2)
1526
+ return named2;
1527
+ const file = f.cite.file.split("/").pop().replace(/\.[^.]+$/, "");
1528
+ return /(Client|Service|Api|Feign|Gateway|Proxy)$/.test(file) ? file : void 0;
1529
+ }
1530
+ function hostOf(f, facts) {
1531
+ const fromUrl = (u) => /^[a-z][a-z0-9+.-]*:\/\/([^/:?#]+)/i.exec(u)?.[1];
1532
+ if (f.host)
1533
+ return fromUrl(f.host) ?? f.host.split(/[/:]/)[0];
1534
+ if (f.base) {
1535
+ const cfg = facts.find((x) => x.kind === "config" && x.key === f.base && !!x.value);
1536
+ if (!cfg?.value)
1537
+ return void 0;
1538
+ return fromUrl(cfg.value) ?? (/^[a-z0-9-]+(\.[a-z0-9-]+)+(:\d+)?$/i.test(cfg.value) ? cfg.value.split(":")[0] : void 0);
1539
+ }
1540
+ return void 0;
1541
+ }
1542
+ var answerLabel = (q) => q.answer?.option !== void 0 ? q.options[q.answer.option - 1]?.label : q.answer?.text;
1543
+ var NOT_A_TARGET = /* @__PURE__ */ new Set(["None of these", TARGETS_SPLIT_LABEL, TARGETS_ANOTHER_LABEL]);
1544
+ function candidates(callerRepo, ctx) {
1545
+ const manifest = estateRepos(ctx.op).filter((r) => r.id !== callerRepo || !!ctx.only);
1546
+ const systems = ctx.op.profiles.flatMap((p) => p.profile?.systems ?? []);
1547
+ const reposOfSystem = /* @__PURE__ */ new Map();
1548
+ for (const r of estateRepos(ctx.op))
1549
+ if (r.system)
1550
+ reposOfSystem.set(r.system, (reposOfSystem.get(r.system) ?? 0) + 1);
1551
+ const read = new Set((ctx.estate ?? []).filter((x) => x.facts.length > 0).map((x) => x.repo));
1552
+ const repoOf = /* @__PURE__ */ new Map();
1553
+ for (const r of ctx.records)
1554
+ if (r.kind === "Component" && r.repo)
1555
+ repoOf.set(r.id, r.repo);
1556
+ if (!ctx.estate)
1557
+ for (const repo of repoOf.values())
1558
+ read.add(repo);
1559
+ const endpoints = ctx.records.filter((r) => r.kind === "Endpoint");
1560
+ const deploys = (ctx.estate ?? []).flatMap((x) => x.facts.filter((f) => f.kind === "deploy" && !!f.basePath));
1561
+ const out = [];
1562
+ for (const m of manifest) {
1563
+ const own2 = new Set([m.id, m.component, ...m.modules.map((x) => x.component)].filter((x) => !!x).map(factIdPart));
1564
+ const sys = m.system && reposOfSystem.get(m.system) === 1 ? systems.find((s) => s.id === m.system) : void 0;
1565
+ const names = /* @__PURE__ */ new Map();
1566
+ for (const n of [m.id, m.component, ...m.modules.map((x) => x.component)])
1567
+ for (const w of nameWords(n))
1568
+ if (!names.has(w))
1569
+ names.set(w, n);
1570
+ for (const a of sys ? [sys.id, ...sys.aliases] : [])
1571
+ for (const w of nameWords(a))
1572
+ if (!names.has(w))
1573
+ names.set(w, a);
1574
+ const bases = [];
1575
+ if (m.basePath)
1576
+ bases.push({ path: m.basePath, from: "the manifest's basePath" });
1577
+ for (const x of m.modules)
1578
+ if (x.basePath)
1579
+ bases.push({ path: x.basePath, from: `the basePath of its module ${x.component}` });
1580
+ for (const d of deploys) {
1581
+ const reaches = d.exposes ? factIdPart(d.exposes) : d.repo === m.id ? m.id : void 0;
1582
+ if (!reaches || !(own2.has(reaches) || d.repo === m.id && !d.exposes))
1583
+ continue;
1584
+ const what2 = d.platform === "apigee" ? "its proxy's BasePath" : `the ${d.resourceType ?? "route"} ${d.resourceName ?? d.id}`;
1585
+ bases.push({ path: d.basePath, from: `${what2} (${d.repo} ${d.cite.file}:${d.cite.line})` });
1586
+ }
1587
+ const comps = [
1588
+ ...new Set(endpoints.filter((e) => repoOf.get(e.component) === m.id).map((e) => e.component))
1589
+ ].sort();
1590
+ const isRead = read.has(m.id);
1591
+ const forLabel = comps.length ? comps : [m.id];
1592
+ for (const label2 of forLabel) {
1593
+ const mine = endpoints.filter((e) => e.component === label2);
1594
+ const entries = (ctx.estate ?? []).filter((x) => x.repo === m.id).flatMap((x) => x.facts.filter((f) => f.kind === "entry" && (!comps.length || factIdPart(f.component ?? m.component ?? m.id) === label2 || comps.length === 1))).map((f) => ({
1595
+ ...f.method ? { method: f.method.toUpperCase() } : {},
1596
+ path: f.path,
1597
+ at: `${f.cite.file}:${f.cite.line}`
1598
+ }));
1599
+ for (const e of mine)
1600
+ if (!entries.some((x) => x.path === e.pathOrTopic))
1601
+ entries.push({
1602
+ ...e.method ? { method: e.method.toUpperCase() } : {},
1603
+ path: e.pathOrTopic,
1604
+ at: e.id
1605
+ });
1606
+ out.push({
1607
+ s: { label: label2, kind: "repo", repo: m.id, read: isRead, score: 0, why: [] },
1608
+ names,
1609
+ bases,
1610
+ entries,
1611
+ ...m.description?.text ? { description: m.description.text } : {},
1612
+ hosts: m.hosts.map((h) => h.toLowerCase())
1613
+ });
1614
+ }
1615
+ }
1616
+ for (const c of ctx.only ?? [])
1617
+ if (!out.some((x) => x.s.label === c))
1618
+ out.push({
1619
+ s: {
1620
+ label: c,
1621
+ kind: "repo",
1622
+ repo: repoOf.get(c) ?? callerRepo,
1623
+ read: true,
1624
+ score: 0,
1625
+ why: []
1626
+ },
1627
+ names: /* @__PURE__ */ new Map(),
1628
+ bases: [],
1629
+ entries: [],
1630
+ hosts: []
1631
+ });
1632
+ const own = new Set(estateRepos(ctx.op).flatMap((r) => r.system ? [r.system] : []));
1633
+ const seen = /* @__PURE__ */ new Set();
1634
+ for (const s of systems) {
1635
+ if (!(s.boundary === "vendor" || !own.has(s.id)) || seen.has(s.id))
1636
+ continue;
1637
+ seen.add(s.id);
1638
+ const names = /* @__PURE__ */ new Map();
1639
+ for (const n of [s.id, s.name, ...s.aliases])
1640
+ for (const w of nameWords(n))
1641
+ if (!names.has(w))
1642
+ names.set(w, n);
1643
+ out.push({
1644
+ s: {
1645
+ label: s.id,
1646
+ kind: "outside",
1647
+ read: false,
1648
+ name: s.name,
1649
+ boundary: s.boundary,
1650
+ score: 0,
1651
+ why: []
1652
+ },
1653
+ names,
1654
+ bases: [],
1655
+ entries: [],
1656
+ hosts: s.aliases.map((a) => a.toLowerCase())
1657
+ });
1658
+ }
1659
+ return out;
1660
+ }
1661
+ function targetSuggestions(f, ctx) {
1662
+ const all = candidates(f.repo, ctx);
1663
+ const df = /* @__PURE__ */ new Map();
1664
+ for (const c of all)
1665
+ for (const w of c.names.keys())
1666
+ df.set(w, (df.get(w) ?? 0) + 1);
1667
+ const telling = (w) => (df.get(w) ?? 0) <= Math.max(2, Math.floor(all.length / 3));
1668
+ const host = hostOf(f, ctx.facts)?.toLowerCase();
1669
+ const client = clientOf(f);
1670
+ const said = [
1671
+ { from: f.base ? `the config key ${f.base}` : "", words: nameWords(f.base), weight: 4 },
1672
+ { from: client ? `the client ${client}` : "", words: nameWords(client), weight: 4 },
1673
+ {
1674
+ from: host ? `the host label ${host.split(".")[0]} (${host})` : "",
1675
+ words: host ? nameWords(host.split(".")[0]) : [],
1676
+ weight: 4
1677
+ },
1678
+ {
1679
+ from: `the path ${f.path}`,
1680
+ words: nameWords(segs(f.path).filter((x) => x !== "{}").join(" ")),
1681
+ weight: 2
1682
+ }
1683
+ ].filter((x) => x.from && x.words.length);
1684
+ const method = f.method?.toUpperCase();
1685
+ const earlier = (ctx.questions ?? []).filter((q) => q.askedBy?.step === TARGETS_STEP_ID && q.state === "answered" && q.repo === f.repo && !q.evidence.includes(`${f.cite.file}:${f.cite.line}`));
1686
+ const only = ctx.only ? new Set(ctx.only) : void 0;
1687
+ const out = [];
1688
+ for (const c of all) {
1689
+ if (only && c.s.kind === "repo" && !only.has(c.s.label))
1690
+ continue;
1691
+ const s = { ...c.s, why: [] };
1692
+ const add = (n, why) => {
1693
+ s.score += n;
1694
+ s.why.push(why);
1695
+ };
1696
+ if (only?.has(c.s.label))
1697
+ add(6, "the join found an entry of it that serves the call");
1698
+ const hit = c.entries.find((e) => (!method || !e.method || e.method === method || e.method === "ANY") && serves(e.path, f.path, c.bases.map((b) => b.path)));
1699
+ if (hit && !only) {
1700
+ const what2 = [hit.method, hit.path].filter(Boolean).join(" ");
1701
+ add(6, `serves ${what2} (${hit.at})`);
1702
+ s.serves = [what2];
1703
+ }
1704
+ const base2 = c.bases.find((b) => under(f.path, b.path));
1705
+ if (base2)
1706
+ add(5, `its basePath ${base2.path} matches: ${base2.from}`);
1707
+ if (host && c.hosts.includes(host))
1708
+ add(8, `it lists ${host} under hosts`);
1709
+ for (const x of said) {
1710
+ const w = x.words.find((y) => c.names.has(y) && telling(y));
1711
+ if (w)
1712
+ add(x.weight, `named like ${x.from}`);
1713
+ }
1714
+ if (c.description) {
1715
+ const d = new Set(nameWords(c.description));
1716
+ const w = said.flatMap((x) => x.words).find((y) => d.has(y) && telling(y));
1717
+ if (w)
1718
+ add(2, `its description mentions ${w}`);
1719
+ }
1720
+ const named2 = earlier.filter((q) => {
1721
+ const l = answerLabel(q);
1722
+ if (!l || NOT_A_TARGET.has(l))
1723
+ return false;
1724
+ const t = slugify(l);
1725
+ return t === slugify(c.s.label) || !!c.s.repo && t === slugify(c.s.repo);
1726
+ }).length;
1727
+ if (named2)
1728
+ add(3, `picked for ${named2} other ${named2 === 1 ? "call" : "calls"} of ${f.repo}`);
1729
+ if (!s.score)
1730
+ s.why.push(TARGETS_NO_EVIDENCE);
1731
+ s.why = [...new Set(s.why)];
1732
+ out.push(s);
1733
+ }
1734
+ return out.sort((a, b) => b.score - a.score || Number(b.read) - Number(a.read) || (a.kind === b.kind ? 0 : a.kind === "repo" ? -1 : 1) || cmpText(a.label, b.label));
1735
+ }
1736
+ function offeredSuggestions(ranked, o) {
1737
+ const max = o.max ?? TARGETS_MAX_SUGGESTED;
1738
+ const strong = ranked.filter((s) => s.score >= 2);
1739
+ const fill = ranked.filter((s) => s.score < 2 && (s.kind === "outside" || s.read && o.serving.has(s.label)));
1740
+ return [...strong, ...fill].slice(0, max);
1741
+ }
1742
+ function targetPicker(op, read = /* @__PURE__ */ new Set()) {
1743
+ const repos = estateRepos(op).map((r) => ({
1744
+ id: r.id,
1745
+ kind: "repo",
1746
+ name: r.component && r.component !== r.id ? `${r.id} (${r.component})` : r.id,
1747
+ read: read.has(r.id)
1748
+ })).sort((a, b) => cmpText(a.id, b.id));
1749
+ const seen = /* @__PURE__ */ new Set();
1750
+ const systems = op.profiles.flatMap((p) => p.profile?.systems ?? []).filter((s) => !seen.has(s.id) && !!seen.add(s.id)).map((s) => ({ id: s.id, kind: "system", name: s.name, boundary: s.boundary })).sort((a, b) => cmpText(a.id, b.id));
1751
+ const owned = new Set(op.profiles.flatMap((p) => (p.profile?.systems ?? []).flatMap((s) => s.owner ? [s.owner] : [])));
1752
+ const teams = (op.roles?.teams ?? []).map((t) => {
1753
+ const name = t.name ?? t.id;
1754
+ const has = owned.has(t.id);
1755
+ return {
1756
+ id: t.id,
1757
+ kind: "team",
1758
+ name,
1759
+ ...has ? {} : {
1760
+ adds: {
1761
+ id: slugify(`${t.id}-system`),
1762
+ name: `${name}'s system`,
1763
+ boundary: "internal",
1764
+ owner: t.id
1765
+ }
1766
+ }
1767
+ };
1768
+ }).sort((a, b) => cmpText(a.id, b.id));
1769
+ return [...repos, ...systems, ...teams];
1770
+ }
1771
+ function pickerLines(picks) {
1772
+ const of = (k) => picks.filter((p) => p.kind === k);
1773
+ return [
1774
+ `Repos (${of("repo").length}): ${of("repo").map((p) => `${p.id}${p.read ? "" : " (not read yet)"}`).join(", ") || "none"}`,
1775
+ `Systems (${of("system").length}): ${of("system").map((p) => `${p.id} (${p.boundary})`).join(", ") || "none"}`,
1776
+ `Teams (${of("team").length}): ${of("team").map((p) => `${p.id}${p.adds ? " (no system yet)" : ""}`).join(", ") || "none"}`
1777
+ ];
1778
+ }
1779
+ function topicQuestionDrafts(repo, records) {
1780
+ const out = /* @__PURE__ */ new Map();
1781
+ for (const r of records) {
1782
+ if (r.kind !== "Unresolved")
1783
+ continue;
1784
+ const x = r;
1785
+ if (x.unresolvedKind !== "event-publish" && x.unresolvedKind !== "event-subscribe")
1786
+ continue;
1787
+ const topic = String(x.raw);
1788
+ if (!topic || topic.includes("${") || /\s/.test(topic))
1789
+ continue;
1790
+ const ev = x.evidence.filter((e) => e.repo === repo && e.path && e.line && !/[\s:]/.test(e.path)).map((e) => `${e.path}:${e.line}`).sort();
1791
+ if (!ev.length)
1792
+ continue;
1793
+ const side = x.unresolvedKind === "event-publish" ? "publish" : "listen";
1794
+ const question = topicQuestionText(topic, x.owner ?? repo, side);
1795
+ const draft = {
1796
+ question,
1797
+ options: [
1798
+ {
1799
+ label: TOPICS_OUTSIDE_LABEL,
1800
+ means: `${topic} belongs to another team's system or a vendor's: name it, or the team, from the list`,
1801
+ changes: `${topic} is added under that system's topics in the profile (or the system is added, owner the team), as a file change for review; the next write ${side === "publish" ? "sends the publish to it" : "takes the listener's messages from it"}, marked as your word`
1802
+ },
1803
+ {
1804
+ label: "None of these",
1805
+ means: `Nothing outside the estate owns ${topic}, or a repo not read yet serves it`,
1806
+ changes: `It stays a loose end, and this isn't asked again`
1807
+ }
1808
+ ],
1809
+ evidence: [...new Set(ev)],
1810
+ scope: "repo"
1811
+ };
1812
+ const id = questionId({ step: TOPICS_STEP_ID, repo, question, evidence: draft.evidence });
1813
+ if (!out.has(id))
1814
+ out.set(id, { topic, id, draft });
1815
+ }
1816
+ return [...out.values()].sort((a, b) => cmpText(a.topic, b.topic));
1817
+ }
1818
+
1415
1819
  // packages/scanner/dist/facts-catalog.js
1416
- var TARGETS_STEP = "facts-targets";
1820
+ var TARGETS_STEP = TARGETS_STEP_ID;
1417
1821
  var TARGETS_NONE = "None of these";
1418
- var byKindId = (a, b) => a.kind === b.kind ? a.id.localeCompare(b.id) : a.kind.localeCompare(b.kind);
1822
+ var byKindId = (a, b) => a.kind === b.kind ? cmpText(a.id, b.id) : cmpText(a.kind, b.kind);
1419
1823
  var URL_RE = /^[a-z][a-z0-9+.-]*:\/\//i;
1420
1824
  var joinUrl = (base2, path) => `${base2.replace(/\/+$/, "")}${path.startsWith("/") ? path : `/${path}`}`;
1421
1825
  function urlOf(f, facts) {
@@ -1433,6 +1837,42 @@ var EVENT_CALLS = /* @__PURE__ */ new Set(["event-topic", "mq-queue"]);
1433
1837
  function needsTarget(f, facts) {
1434
1838
  return f.kind === "call" && !f.to && !EVENT_CALLS.has(f.callType) && !(HTTP_CALLS.has(f.callType) && urlOf(f, facts));
1435
1839
  }
1840
+ var UNRESOLVED_HOST = /@[A-Za-z_][A-Za-z0-9_.-]*@|\$\{|\{[^}]*\}/;
1841
+ function ruleMayName(f, facts) {
1842
+ if (f.kind !== "call" || EVENT_CALLS.has(f.callType))
1843
+ return false;
1844
+ if (!f.to && !(HTTP_CALLS.has(f.callType) && urlOf(f, facts)))
1845
+ return true;
1846
+ if (f.to)
1847
+ return f.by !== "person" && UNRESOLVED_HOST.test(f.to);
1848
+ if (!HTTP_CALLS.has(f.callType))
1849
+ return false;
1850
+ const url = urlOf(f, facts);
1851
+ const host = url ? /^[a-z][a-z0-9+.-]*:\/\/([^/?#]*)/i.exec(url)?.[1] ?? "" : "";
1852
+ if (!host || UNRESOLVED_HOST.test(host) || UNRESOLVED_HOST.test(f.host ?? ""))
1853
+ return true;
1854
+ if (f.host || !f.base)
1855
+ return false;
1856
+ const hosts = new Set(facts.filter((x) => x.kind === "config" && x.key === f.base && !!x.value).map((x) => URL_RE.test(x.value) ? urlHostPort(x.value)?.host : x.value.trim()).filter((h) => !!h));
1857
+ return hosts.size > 1;
1858
+ }
1859
+ var MAX_ID = 120;
1860
+ function remapIds(remap) {
1861
+ return (id) => {
1862
+ const exact = remap.get(id);
1863
+ if (exact)
1864
+ return exact;
1865
+ for (const [from, to] of remap)
1866
+ if (id.startsWith(`${from}.`)) {
1867
+ const out = to + id.slice(from.length);
1868
+ if (out.length <= MAX_ID)
1869
+ return out;
1870
+ const hash = createHash2("sha256").update(out, "utf8").digest("hex").slice(0, 16);
1871
+ return `${out.slice(0, MAX_ID - hash.length - 1).replace(/[-._]+$/, "")}.${hash}`;
1872
+ }
1873
+ return id;
1874
+ };
1875
+ }
1436
1876
  function targetQuestionFor(f) {
1437
1877
  if (f.kind !== "call")
1438
1878
  return void 0;
@@ -1449,81 +1889,425 @@ var SERVES = {
1449
1889
  webhook: ["webhook"],
1450
1890
  sftp: ["file-drop"]
1451
1891
  };
1452
- function targetQuestionDrafts(facts, records, op, asks) {
1892
+ function targetAsks(facts, records, asks) {
1453
1893
  const repoOf = /* @__PURE__ */ new Map();
1454
1894
  for (const r of records)
1455
1895
  if (r.kind === "Component" && r.repo)
1456
1896
  repoOf.set(r.id, r.repo);
1457
- const vendors = op.profiles.flatMap((p) => p.profile?.systems ?? []).filter((s) => s.boundary === "vendor").sort((a, b) => a.id.localeCompare(b.id));
1897
+ const list = [];
1898
+ for (const f of [...facts].sort((a, b) => cmpText(a.id, b.id))) {
1899
+ const ask = asks?.find((a) => a.reason === "ambiguous" && a.call.repo === f.repo && a.call.fact === f.id);
1900
+ if (f.kind !== "call" || !needsTarget(f, facts) && !ask)
1901
+ continue;
1902
+ const own = slugify(f.component ?? f.repo).replace(/\./g, "-");
1903
+ const types = new Set(SERVES[f.callType] ?? []);
1904
+ const servers = ask ? [...new Set(ask.candidates.map((c) => c.component))].sort() : [
1905
+ ...new Set(records.filter((r) => r.kind === "Endpoint" && types.has(r.endpointType) && r.component !== own).filter((r) => repoOf.get(r.component) !== f.repo).map((r) => r.component))
1906
+ ].sort();
1907
+ list.push({ f, q: targetQuestionFor(f), servers });
1908
+ }
1909
+ return { list, repoOf };
1910
+ }
1911
+ var outsideAs = (s) => s.boundary === "vendor" ? "an outside system" : "another team's system";
1912
+ var callWhat = (f) => [f.method?.toUpperCase(), f.path].filter(Boolean).join(" ");
1913
+ function suggestionWhy(s, prefix) {
1914
+ const served = s.serves ?? [];
1915
+ let why = s.why;
1916
+ if (prefix !== void 0 && served.length > 1) {
1917
+ const under2 = prefix === "/" ? "" : ` under ${prefix}`;
1918
+ why = [
1919
+ `serves ${served[0]} and ${served.length - 1} more${under2}`,
1920
+ ...why.filter((w) => !w.startsWith("serves "))
1921
+ ];
1922
+ }
1923
+ return (why.length ? why : [TARGETS_NO_EVIDENCE]).slice(0, 3).join("; ");
1924
+ }
1925
+ function suggestionOption(s, callerRepo, n, prefix) {
1926
+ const why = suggestionWhy(s, prefix);
1927
+ if (s.kind === "outside")
1928
+ return {
1929
+ label: s.label,
1930
+ means: `${s.name}, ${outsideAs({ boundary: s.boundary ?? "vendor" })}: ${why}`,
1931
+ changes: n ? `The ${n} calls go to ${s.name} when the facts are next written` : `The call goes to ${s.name} when the facts are next written`
1932
+ };
1933
+ const who = s.label === s.repo ? `${s.repo}${s.read ? "" : " (not read yet)"}` : `${s.label}, built in ${s.repo}${s.read ? "" : " (not read yet)"}`;
1934
+ const them = n ? `The ${n} calls` : "The call";
1935
+ return {
1936
+ label: s.label,
1937
+ means: `${who}: ${why}`,
1938
+ changes: s.read ? `${them} ${n ? "join" : "joins"} ${s.label} when the facts are next written (atticus verify ${callerRepo} --write)` : `${them} ${n ? "go" : "goes"} to ${s.repo} when the facts are next written (atticus verify ${callerRepo} --write), and ${n ? "join" : "joins"} its route once ${s.repo} is read and verified (/atticus-map ${s.repo}); if no route of it serves ${n ? "them" : "it"}, the question opens again`
1939
+ };
1940
+ }
1941
+ var anotherOption = (op) => {
1942
+ const repos = estateRepos(op).length;
1943
+ const systems = new Set(op.profiles.flatMap((p) => (p.profile?.systems ?? []).map((s) => s.id))).size;
1944
+ const teams = op.roles?.teams?.length ?? 0;
1945
+ return {
1946
+ label: TARGETS_ANOTHER,
1947
+ means: `A repo, system or team not listed here: the manifest's ${repos} repos (read or not), the profiles' ${systems} systems and the ${teams} teams in roles.yaml`,
1948
+ changes: "Nothing until you name one: atticus questions answer <id> --target <repo, system or team id> lists them and takes the one you name (the app's Questions page lists them too); naming a vendor or a team with no system adds that system to the profile for review"
1949
+ };
1950
+ };
1951
+ function offered(members, facts, records, op, asks, ctx) {
1952
+ const merged = /* @__PURE__ */ new Map();
1953
+ for (const m of members) {
1954
+ const ambiguous = asks?.some((a) => a.reason === "ambiguous" && a.call.repo === m.f.repo && a.call.fact === m.f.id);
1955
+ for (const s of targetSuggestions(m.f, {
1956
+ op,
1957
+ records,
1958
+ facts,
1959
+ ...ctx.estate ? { estate: ctx.estate } : {},
1960
+ ...ctx.questions ? { questions: ctx.questions } : {},
1961
+ ...ambiguous ? { only: m.servers } : {}
1962
+ })) {
1963
+ const x = merged.get(s.label);
1964
+ if (!x)
1965
+ merged.set(s.label, { ...s, why: [...s.why], serves: [...s.serves ?? []] });
1966
+ else {
1967
+ x.score += s.score;
1968
+ for (const w of s.why)
1969
+ if (!x.why.includes(w))
1970
+ x.why.push(w);
1971
+ for (const w of s.serves ?? [])
1972
+ if (!x.serves.includes(w))
1973
+ x.serves.push(w);
1974
+ if (x.why.length > 1)
1975
+ x.why = x.why.filter((w) => w !== TARGETS_NO_EVIDENCE);
1976
+ }
1977
+ }
1978
+ }
1979
+ const ranked = [...merged.values()].sort((a, b) => b.score - a.score || Number(b.read) - Number(a.read) || (a.kind === b.kind ? 0 : a.kind === "repo" ? -1 : 1) || cmpText(a.label, b.label));
1980
+ const serving = new Set(members.flatMap((m) => m.servers));
1981
+ const types = new Set(members.flatMap((m) => SERVES[m.f.callType] ?? []));
1982
+ return offeredSuggestions(ranked, { types, serving });
1983
+ }
1984
+ function targetQuestionDrafts(facts, records, op, asks, ctx = {}) {
1985
+ const { list } = targetAsks(facts, records, asks);
1986
+ return list.map((m) => {
1987
+ const { f, q } = m;
1988
+ const draft = {
1989
+ question: q.question,
1990
+ options: [
1991
+ ...offered([m], facts, records, op, asks, ctx).map((s) => suggestionOption(s, f.repo)),
1992
+ anotherOption(op),
1993
+ {
1994
+ label: TARGETS_NONE,
1995
+ means: "Nothing in the estate or the org pack's systems answers it",
1996
+ changes: "The call stays a loose end, and this isn't asked again"
1997
+ }
1998
+ ],
1999
+ evidence: q.evidence,
2000
+ scope: "repo"
2001
+ };
2002
+ return {
2003
+ factId: f.id,
2004
+ id: questionId({
2005
+ step: TARGETS_STEP,
2006
+ repo: f.repo,
2007
+ question: q.question,
2008
+ evidence: q.evidence
2009
+ }),
2010
+ repo: f.repo,
2011
+ draft
2012
+ };
2013
+ });
2014
+ }
2015
+ var TARGETS_SPLIT = TARGETS_SPLIT_LABEL;
2016
+ var PREFIX_ROOT = /^(api|rest|services?|v\d+(\.\d+)*)$/i;
2017
+ function targetPathPrefix(path) {
2018
+ const lead = path.startsWith("/") ? "/" : "";
2019
+ const out = [];
2020
+ for (const s of path.split(/[?#]/)[0].split("/").filter(Boolean)) {
2021
+ if (/^[{:<]/.test(s))
2022
+ break;
2023
+ out.push(s);
2024
+ if (!PREFIX_ROOT.test(s))
2025
+ break;
2026
+ }
2027
+ return out.length ? `${lead}${out.join("/")}` : "/";
2028
+ }
2029
+ function groupVia(f) {
2030
+ if (f.to)
2031
+ return ` naming ${f.to}`;
2032
+ if (f.host)
2033
+ return ` on ${f.host}`;
2034
+ if (f.base)
2035
+ return ` through ${f.base}`;
2036
+ return "";
2037
+ }
2038
+ function targetGroupQuestion(repo, f, facts) {
2039
+ const proxy = facts ? proxyOf(f, facts) : void 0;
2040
+ const through = f.base ?? f.host ?? f.to;
2041
+ if (proxy && through) {
2042
+ const p = targetPathPrefix(f.path);
2043
+ return `Which repo serves the routes under ${p === "/" ? "" : p}/** that ${proxy} forwards to ${through}?`;
2044
+ }
2045
+ return `Which repo or system answers the calls ${repo} makes under ${targetPathPrefix(f.path)}${groupVia(f)}?`;
2046
+ }
2047
+ function proxyOf(f, facts) {
2048
+ if (!f.from)
2049
+ return void 0;
2050
+ const from = facts.find((x) => x.id === f.from);
2051
+ if (!from || from.kind !== "entry")
2052
+ return void 0;
2053
+ const proxy = facts.find((x) => x.kind === "deploy" && x.repo === f.repo && x.platform === "apigee" && !!x.basePath);
2054
+ return proxy ? proxy.resourceName ?? f.repo : void 0;
2055
+ }
2056
+ function targetGroupWordings(repo, f, facts) {
2057
+ return [.../* @__PURE__ */ new Set([targetGroupQuestion(repo, f, facts), targetGroupQuestion(repo, f)])];
2058
+ }
2059
+ var repo0 = (members) => members[0].f.repo;
2060
+ var byLine = (a, b) => cmpNatural(a, b);
2061
+ var answerLabel2 = (q) => q.answer?.option !== void 0 ? q.options[q.answer.option - 1]?.label : q.answer?.text;
2062
+ function splitTargetGroups(questions) {
2063
+ return new Set(questions.filter((q) => q.askedBy?.step === TARGETS_STEP && q.state === "answered" && answerLabel2(q) === TARGETS_SPLIT).map((q) => q.question.trim()));
2064
+ }
2065
+ function groupedTargetDrafts(facts, records, op, asks, o) {
2066
+ const { list } = targetAsks(facts, records, asks);
2067
+ const ctx = {
2068
+ ...o.estate ? { estate: o.estate } : {},
2069
+ ...o.questions ? { questions: o.questions } : {}
2070
+ };
2071
+ const own = new Map(targetQuestionDrafts(facts, records, op, asks, ctx).map((d) => [d.factId, d]));
2072
+ const groups = /* @__PURE__ */ new Map();
2073
+ for (const a of list) {
2074
+ if (!o.ask.has(a.f.id))
2075
+ continue;
2076
+ const k = targetGroupQuestion(a.f.repo, a.f, facts);
2077
+ groups.set(k, [...groups.get(k) ?? [], a]);
2078
+ }
1458
2079
  const out = [];
1459
- for (const f of [...facts].sort((a, b) => a.id.localeCompare(b.id))) {
1460
- const ask = asks?.find((a) => a.reason === "ambiguous" && a.call.repo === f.repo && a.call.fact === f.id);
1461
- if (f.kind !== "call" || !needsTarget(f, facts) && !ask)
2080
+ for (const [question, members] of groups) {
2081
+ const split = targetGroupWordings(repo0(members), members[0].f, facts).some((w) => o.split?.has(w));
2082
+ if (members.length === 1 || split) {
2083
+ for (const m of members)
2084
+ out.push({ ...own.get(m.f.id), factIds: [m.f.id] });
1462
2085
  continue;
1463
- const q = targetQuestionFor(f);
1464
- const own = slugify(f.component ?? f.repo).replace(/\./g, "-");
1465
- const types = new Set(SERVES[f.callType] ?? []);
1466
- const servers = ask ? [...new Set(ask.candidates.map((c) => c.component))].sort() : [
1467
- ...new Set(records.filter((r) => r.kind === "Endpoint" && types.has(r.endpointType) && r.component !== own).filter((r) => repoOf.get(r.component) !== f.repo).map((r) => r.component))
1468
- ].sort();
1469
- const what = [f.method?.toUpperCase(), f.path].filter(Boolean).join(" ");
2086
+ }
2087
+ const repo = members[0].f.repo;
2088
+ const whats = [...new Set(members.map((m) => callWhat(m.f)))];
2089
+ const listed = whats.length > 5 ? `${whats.slice(0, 5).join(", ")} and ${whats.length - 5} more` : whats.join(", ");
2090
+ const n = members.length;
2091
+ const evidence = [...new Set(members.flatMap((m) => m.q.evidence))].sort(byLine);
1470
2092
  const draft = {
1471
- question: q.question,
2093
+ question,
1472
2094
  options: [
1473
- ...servers.map((c) => ({
1474
- label: c,
1475
- means: `${c}${repoOf.get(c) ? `, built in ${repoOf.get(c)},` : ""} answers ${what}`,
1476
- changes: `The call joins ${c} when the facts are next written (atticus verify ${f.repo} --write)`
1477
- })),
1478
- ...vendors.map((s) => ({
1479
- label: s.id,
1480
- means: `${s.name}, an outside system, answers ${what}`,
1481
- changes: `The call goes to ${s.name} when the facts are next written`
1482
- })),
2095
+ ...offered(members, facts, records, op, asks, ctx).map((s) => suggestionOption(s, repo, n, targetPathPrefix(members[0].f.path))),
2096
+ anotherOption(op),
2097
+ {
2098
+ label: TARGETS_SPLIT,
2099
+ means: `The ${n} calls (${listed}) don't all go to one place`,
2100
+ changes: "Nothing is joined from this answer; each call is asked on its own when the facts are next written"
2101
+ },
1483
2102
  {
1484
2103
  label: TARGETS_NONE,
1485
- means: "Nothing in the estate or the org pack's systems answers it",
1486
- changes: "The call stays a loose end, and this isn't asked again"
2104
+ means: "Nothing in the estate or the org pack's systems answers any of them",
2105
+ changes: "The calls stay loose ends, and this isn't asked again"
1487
2106
  }
1488
2107
  ],
1489
- evidence: q.evidence,
2108
+ evidence,
1490
2109
  scope: "repo"
1491
2110
  };
1492
2111
  out.push({
1493
- factId: f.id,
1494
- id: questionId({
1495
- step: TARGETS_STEP,
1496
- repo: f.repo,
1497
- question: q.question,
1498
- evidence: q.evidence
1499
- }),
1500
- repo: f.repo,
2112
+ factId: members[0].f.id,
2113
+ factIds: members.map((m) => m.f.id),
2114
+ id: questionId({ step: TARGETS_STEP, repo, question, evidence }),
2115
+ repo,
1501
2116
  draft
1502
2117
  });
1503
2118
  }
2119
+ return out.sort((a, b) => cmpText(a.factId, b.factId));
2120
+ }
2121
+ function targetRuleOffers(questions, factsOf) {
2122
+ const byKey = /* @__PURE__ */ new Map();
2123
+ for (const q of questions) {
2124
+ if (q.state !== "open" || q.askedBy?.step !== TARGETS_STEP)
2125
+ continue;
2126
+ const facts = factsOf(q.repo) ?? [];
2127
+ const at3 = new Set(q.evidence);
2128
+ const calls = facts.filter((f) => f.kind === "call" && f.repo === q.repo && at3.has(`${f.cite.file}:${f.cite.line}`));
2129
+ if (!calls.length)
2130
+ continue;
2131
+ const prefixes = new Set(calls.map((c) => targetPathPrefix(c.path)));
2132
+ const hosts = new Set(calls.map((c) => c.host?.replace(/^[a-z][a-z0-9+.-]*:\/\//i, "").split(/[/:]/)[0]));
2133
+ const [prefix] = prefixes;
2134
+ const [host] = hosts;
2135
+ const match = prefixes.size === 1 && prefix !== "/" ? `${prefix}/**` : hosts.size === 1 && host ? host : void 0;
2136
+ if (!match)
2137
+ continue;
2138
+ const key = `${q.repo}
2139
+ ${match}`;
2140
+ const x = byKey.get(key) ?? { match, ids: [], calls: 0 };
2141
+ x.ids.push(q.id);
2142
+ x.calls += calls.length;
2143
+ byKey.set(key, x);
2144
+ }
2145
+ const out = /* @__PURE__ */ new Map();
2146
+ for (const x of byKey.values()) {
2147
+ if (x.ids.length < 2)
2148
+ continue;
2149
+ const ids = [...x.ids].sort();
2150
+ for (const id of ids)
2151
+ out.set(id, { match: x.match, ids, calls: x.calls });
2152
+ }
2153
+ return out;
2154
+ }
2155
+ var segmentsOf = (p) => p.split(/[?#]/)[0].split("/").filter(Boolean).map((x) => /^[{:<]/.test(x) ? "{}" : x);
2156
+ function ruleCoversCall(match, f, facts, o = {}) {
2157
+ if (isPathMatch(match)) {
2158
+ const want = segmentsOf(match.replace(/\/\*\*$/, ""));
2159
+ const got = segmentsOf(f.path);
2160
+ return want.length <= got.length && want.every((x, i) => x === "{}" || !o.strict && got[i] === "{}" || x === got[i]);
2161
+ }
2162
+ const url = urlOf(f, facts);
2163
+ const hp = url ? urlHostPort(url) : void 0;
2164
+ return !!hp?.host && hostMatches(match, hp.host, hp.port);
2165
+ }
2166
+ function ruledTargets(repo, facts, questions, o = {}) {
2167
+ const rules = questions.filter((q) => q.askedBy?.step === TARGETS_STEP && q.state === "answered" && !!q.answer?.match && (q.answer.scope.level === "repo" ? q.answer.scope.id === repo : !!o.repoType && q.answer.scope.id === o.repoType)).sort((a, b) => (a.answer.scope.level === "repo" ? 0 : 1) - (b.answer.scope.level === "repo" ? 0 : 1) || b.answer.match.length - a.answer.match.length || cmpText(b.answer.at, a.answer.at) || cmpText(a.id, b.id));
2168
+ const out = /* @__PURE__ */ new Map();
2169
+ if (!rules.length)
2170
+ return out;
2171
+ for (const f of facts) {
2172
+ if (!ruleMayName(f, facts))
2173
+ continue;
2174
+ const q = rules.find((r) => ruleCoversCall(r.answer.match, f, facts, o.strict ? { strict: true } : {}));
2175
+ const label2 = q ? answerLabel2(q) : void 0;
2176
+ if (q && label2 && label2 !== TARGETS_SPLIT)
2177
+ out.set(f.id, { id: q.id, match: q.answer.match, label: label2 });
2178
+ }
1504
2179
  return out;
1505
2180
  }
1506
- function targetsFromAnswers(repo, facts, questions) {
2181
+ function targetsFromAnswers(repo, facts, questions, o = {}) {
1507
2182
  const byId = new Map(questions.map((q) => [q.id, q]));
2183
+ const groups = /* @__PURE__ */ new Map();
2184
+ for (const q of [...questions].filter((x) => x.askedBy?.step === TARGETS_STEP && x.state === "answered" && !!x.answer && typeof x.question === "string").sort((a, b) => cmpText(a.answer.at, b.answer.at) || cmpText(a.id, b.id)))
2185
+ groups.set(q.question.trim(), q);
2186
+ const ruled = ruledTargets(repo, facts, questions, o);
1508
2187
  const out = {};
1509
- for (const f of [...facts].sort((a, b) => a.id.localeCompare(b.id))) {
2188
+ for (const f of [...facts].sort((a, b) => cmpText(a.id, b.id))) {
1510
2189
  if (f.kind !== "call")
1511
2190
  continue;
1512
2191
  const q = targetQuestionFor(f);
1513
2192
  if (!q)
1514
2193
  continue;
1515
- const asked = byId.get(questionId({ step: TARGETS_STEP, repo, ...q }));
1516
- if (!asked || asked.state !== "answered" || !asked.answer)
2194
+ let asked = byId.get(questionId({ step: TARGETS_STEP, repo, ...q }));
2195
+ if (!asked || asked.state !== "answered" || !asked.answer) {
2196
+ const g = targetGroupWordings(repo, f, facts).map((w) => groups.get(w)).find((x) => !!x);
2197
+ asked = g && (needsTarget(f, facts) || g.evidence.includes(q.evidence[0])) ? g : void 0;
2198
+ }
2199
+ let label2 = asked && asked.state === "answered" && asked.answer ? answerLabel2(asked) : void 0;
2200
+ let byAi = label2 && asked?.answer && isAiBy(asked.answer.by) ? asked.answer.by : void 0;
2201
+ if (!label2 || label2 === TARGETS_SPLIT || label2 === TARGETS_ANOTHER) {
2202
+ label2 = ruled.get(f.id)?.label;
2203
+ byAi = void 0;
2204
+ }
2205
+ if (!label2 || label2 === TARGETS_SPLIT || label2 === TARGETS_ANOTHER)
1517
2206
  continue;
1518
- const label2 = asked.answer.option !== void 0 ? asked.options[asked.answer.option - 1]?.label : asked.answer.text;
1519
- if (!label2 || label2 === TARGETS_NONE)
2207
+ if (byAi && o.ai)
2208
+ o.ai[f.id] = { runtime: byAi.runtime, run: byAi.run };
2209
+ if (label2 === TARGETS_NONE) {
2210
+ out[f.id] = NO_TARGET;
1520
2211
  continue;
2212
+ }
1521
2213
  const to = slugify(label2);
1522
2214
  if (to)
1523
2215
  out[f.id] = to;
1524
2216
  }
1525
2217
  return out;
1526
2218
  }
2219
+ function answeredQuestionOfCall(repo, facts, questions, factId) {
2220
+ const f = facts.find((x) => x.id === factId);
2221
+ if (!f || f.kind !== "call")
2222
+ return void 0;
2223
+ const own = targetQuestionFor(f);
2224
+ const ownId = questionId({ step: TARGETS_STEP, repo, ...own });
2225
+ const answered = questions.filter((q2) => q2.askedBy?.step === TARGETS_STEP && q2.state === "answered" && q2.repo === repo);
2226
+ const q = answered.find((x) => x.id === ownId) ?? answered.find((x) => targetGroupWordings(repo, f, facts).includes(x.question.trim()) && x.evidence.includes(own.evidence[0]));
2227
+ return q?.answer?.match ? void 0 : q;
2228
+ }
2229
+ function answerReconfirmed(q) {
2230
+ if (!q.answer || !q.reopened)
2231
+ return false;
2232
+ const now2 = answerLabel2(q);
2233
+ const was = answerLabel2({ ...q, answer: q.reopened.answer });
2234
+ return !!now2 && !!was && slugify(now2) === slugify(was);
2235
+ }
2236
+ var JOINED_BY = {
2237
+ named: "the call names it",
2238
+ repo: "the call names its repo",
2239
+ host: "the manifest's hosts and base paths decide it",
2240
+ topic: "it listens on the call's topic"
2241
+ };
2242
+ function callsOfTargetQuestion(repo, facts, q) {
2243
+ return facts.filter((f) => {
2244
+ if (f.kind !== "call")
2245
+ return false;
2246
+ const own = targetQuestionFor(f);
2247
+ if (questionId({ step: TARGETS_STEP, repo, ...own }) === q.id)
2248
+ return true;
2249
+ return targetGroupWordings(repo, f, facts).includes(q.question.trim()) && q.evidence.includes(own.evidence[0]);
2250
+ });
2251
+ }
2252
+ function staleTargetQuestions(input) {
2253
+ const drafted = new Set(input.drafted.map((d) => d.id));
2254
+ const out = [];
2255
+ for (const q of input.open) {
2256
+ if (q.state !== "open" || q.askedBy.step !== TARGETS_STEP || drafted.has(q.id))
2257
+ continue;
2258
+ const calls = callsOfTargetQuestion(input.repo, input.facts, q);
2259
+ if (!calls.length)
2260
+ continue;
2261
+ const ids = new Set(calls.map((c) => c.id));
2262
+ const by = input.drafted.filter((d) => d.factIds.some((x) => ids.has(x))).map((d) => d.id);
2263
+ if (by.length) {
2264
+ out.push({ id: q.id, why: `its calls are asked now in ${by.join(", ")}`, replacedBy: by });
2265
+ continue;
2266
+ }
2267
+ if (calls.some((c) => input.asked.has(c.id)))
2268
+ continue;
2269
+ const reasons = calls.map((c) => {
2270
+ const rule = input.ruled?.get(c.id);
2271
+ if (rule)
2272
+ return `${callWhat(c)} is answered by rule in ${rule.id} (${rule.match})`;
2273
+ const j = input.join?.joins.find((x) => x.call.repo === input.repo && x.call.fact === c.id && x.how !== "answer");
2274
+ if (j && !isGuessedJoin(j) && JOINED_BY[j.how])
2275
+ return `${callWhat(c)} joins ${j.entry.component} in ${j.entry.repo}: ${JOINED_BY[j.how]}`;
2276
+ if (!j && c.to)
2277
+ return `${callWhat(c)} now names ${c.to} in the code`;
2278
+ return void 0;
2279
+ });
2280
+ const rules = [
2281
+ ...new Set(calls.flatMap((c) => input.ruled?.get(c.id) ? [input.ruled.get(c.id).id] : []))
2282
+ ].sort();
2283
+ if (reasons.every((r) => r !== void 0))
2284
+ out.push({
2285
+ id: q.id,
2286
+ why: reasons.join("; "),
2287
+ ...rules.length ? { replacedBy: rules } : {}
2288
+ });
2289
+ }
2290
+ return out.sort((a, b) => cmpText(a.id, b.id));
2291
+ }
2292
+ function staleTopicQuestions(input) {
2293
+ const drafted = new Set(input.drafted.map((d) => d.id));
2294
+ const out = [];
2295
+ for (const q of input.open) {
2296
+ if (q.state !== "open" || q.askedBy.step !== TOPICS_STEP_ID || q.repo !== input.repo)
2297
+ continue;
2298
+ if (drafted.has(q.id))
2299
+ continue;
2300
+ const topic = topicOfQuestion(q);
2301
+ if (!topic)
2302
+ continue;
2303
+ const by = input.drafted.filter((d) => d.topic === topic).map((d) => d.id);
2304
+ out.push(by.length ? { id: q.id, why: `${topic} is asked now in ${by.join(", ")}`, replacedBy: by } : {
2305
+ id: q.id,
2306
+ why: `${topic} is no longer a loose end: a repo of the estate serves its other side now, or the profile names who owns it`
2307
+ });
2308
+ }
2309
+ return out.sort((a, b) => cmpText(a.id, b.id));
2310
+ }
1527
2311
  function componentRenames(repos, op, taken = []) {
1528
2312
  const manifests = estateRepos(op);
1529
2313
  const users = /* @__PURE__ */ new Map();
@@ -1538,7 +2322,7 @@ function componentRenames(repos, op, taken = []) {
1538
2322
  if (users.has(t.id))
1539
2323
  use(t.id, t.repo);
1540
2324
  const out = {};
1541
- for (const [id, set] of [...users].sort((a, b) => a[0].localeCompare(b[0]))) {
2325
+ for (const [id, set] of [...users].sort((a, b) => cmpText(a[0], b[0]))) {
1542
2326
  if (set.size < 2)
1543
2327
  continue;
1544
2328
  for (const repo of repos.map((r) => r.repo).filter((r) => set.has(r))) {
@@ -1566,7 +2350,7 @@ function factsRecords(input) {
1566
2350
  const facts = withComponents(repo, input.facts, op, input.renames).map((f) => {
1567
2351
  let g = f;
1568
2352
  const to = input.targets?.[g.id];
1569
- if (g.kind === "call" && !g.to && to)
2353
+ if (g.kind === "call" && !g.to && to && to !== NO_TARGET)
1570
2354
  g = { ...g, to };
1571
2355
  return g;
1572
2356
  });
@@ -1613,20 +2397,33 @@ function factsRecords(input) {
1613
2397
  return { facts, projected, factById, newRecords, newFlows, loose, looseFact };
1614
2398
  }
1615
2399
  function outsideSystems(op) {
1616
- return op.profiles.flatMap((p) => p.profile?.systems ?? []).filter((s) => s.boundary === "vendor").flatMap((s) => [s.id, s.name, ...s.aliases]).sort();
2400
+ return outsideProfileSystems(op).flatMap((s) => [s.id, s.name, ...s.aliases]).sort();
2401
+ }
2402
+ function outsideProfileSystems(op) {
2403
+ const own = new Set(estateRepos(op).flatMap((r) => r.system ? [r.system] : []));
2404
+ const seen = /* @__PURE__ */ new Set();
2405
+ return op.profiles.flatMap((p) => p.profile?.systems ?? []).filter((s) => (s.boundary === "vendor" || !own.has(s.id)) && !seen.has(s.id) && !!seen.add(s.id)).sort((a, b) => a.id < b.id ? -1 : a.id > b.id ? 1 : 0);
1617
2406
  }
2407
+ var hostAliases = (aliases) => aliases.filter((a) => /^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)+$/i.test(a)).map((a) => a.toLowerCase());
1618
2408
  function joinReposOf(op, list, renames) {
1619
2409
  const manifests = estateRepos(op);
1620
2410
  return list.map(({ repo, facts }) => {
1621
2411
  const m = manifests.find((r) => r.id === repo);
1622
- const modules = (m?.modules ?? []).filter((x) => x.hosts.length > 0);
2412
+ const modules = (m?.modules ?? []).filter((x) => x.hosts.length > 0 || x.basePath && (m?.hosts.length ?? 0) > 0).map((x) => x.hosts.length ? x : { ...x, hosts: m.hosts });
1623
2413
  const component = m?.component ? renames?.[repo]?.[factIdPart(m.component)] ?? m.component : void 0;
1624
2414
  return {
1625
2415
  repo,
1626
2416
  facts: renames?.[repo] ? withComponents(repo, facts, op, renames) : facts,
1627
2417
  ...component ? { component } : {},
1628
2418
  ...m?.hosts.length ? { hosts: m.hosts } : {},
1629
- ...modules.length ? { modules: modules.map((x) => ({ component: x.component, hosts: x.hosts })) } : {},
2419
+ ...m?.basePath ? { basePath: m.basePath } : {},
2420
+ ...modules.length ? {
2421
+ modules: modules.map((x) => ({
2422
+ component: x.component,
2423
+ hosts: x.hosts,
2424
+ ...x.basePath ? { basePath: x.basePath } : {}
2425
+ }))
2426
+ } : {},
1630
2427
  // An API gateway's entries are routes the join follows through (ADR 0124 addendum A-gateway).
1631
2428
  // A type that says which side it faces (`gateway-external`, `gateway-internal`) marks each
1632
2429
  // hop through it (A-gateway2); a plain `gateway` leaves that to the proxy's virtual host.
@@ -1668,12 +2465,7 @@ function applyFactJoins(records, join10, repos, withCallers) {
1668
2465
  } else if (!(r.kind === "FieldMapping" && ifaces.has(r.interface)))
1669
2466
  kept2.push(r);
1670
2467
  }
1671
- const re = (id) => {
1672
- for (const [from, to] of was)
1673
- if (id === from || id.startsWith(`${from}.`))
1674
- return to + id.slice(from.length);
1675
- return id;
1676
- };
2468
+ const re = remapIds(was);
1677
2469
  const joined = joinRecords(join10, repos);
1678
2470
  const callers = withCallers ? callerRecords(withCallers.callers, repos, withCallers.join, {
1679
2471
  ...withCallers.only ? { only: withCallers.only } : {},
@@ -1694,20 +2486,28 @@ function applyFactJoins(records, join10, repos, withCallers) {
1694
2486
  }
1695
2487
  var entryText = (j) => j.method ? `${j.method} ${j.path}` : j.path;
1696
2488
  function joinAbout(all, repo) {
1697
- const joins = all.joins.filter((j) => j.call.repo === repo || j.entry.repo === repo || !!j.gateway?.some((h) => h.entry.repo === repo));
2489
+ const joins = all.joins.filter((j) => j.call.repo === repo || j.entry.repo === repo || !!j.gateway?.some((h) => h.entry.repo === repo) || // J0: a join whose placeholder this repo's config sets (a deploy or config repo) is
2490
+ // written again with it, so a changed value moves the link.
2491
+ [...j.through ?? [], ...j.entryThrough ?? []].some((t) => t.repo === repo) || // J1: and a join whose host or base path this repo's Route gives (a deploy repo's).
2492
+ [...j.exposedBy ?? [], ...(j.gateway ?? []).flatMap((h) => h.exposedBy ?? [])].some((t) => t.repo === repo));
1698
2493
  const ifaces = new Set(joins.map((j) => j.interface));
2494
+ const outside = (all.outside ?? []).filter((o) => o.call.repo === repo);
2495
+ const exposure = (all.exposure ?? []).filter((x) => x.exposure.repo === repo || "repo" in x && x.repo === repo);
1699
2496
  return {
1700
2497
  joins,
1701
2498
  asks: all.asks.filter((a) => a.call.repo === repo),
1702
2499
  misses: all.misses.filter((m) => m.call.repo === repo),
2500
+ ...outside.length ? { outside } : {},
2501
+ ...exposure.length ? { exposure } : {},
1703
2502
  lineage: all.lineage.filter((l) => ifaces.has(l.interface)),
1704
2503
  gaps: all.gaps.filter((g) => ifaces.has(g.interface))
1705
2504
  };
1706
2505
  }
1707
- function catalogWithFacts(input) {
1708
- const { repo, op } = input;
2506
+ function catalogWithFacts(given) {
2507
+ const { repo, op } = given;
1709
2508
  if (isPrototypeRepo(op, repo))
1710
- return prototypeLeftOut(input);
2509
+ return prototypeLeftOut(given);
2510
+ const input = { ...given, existing: stripSharedMaps(stripLinkEvidence(given.existing)) };
1711
2511
  const others = (input.estate ?? []).filter((x) => x.repo !== repo && !isPrototypeRepo(op, x.repo));
1712
2512
  const renames = componentRenames([{ repo, facts: input.facts }, ...others], op, input.existing.flatMap((r) => r.kind === "Component" && r.repo && r.repo !== repo ? [{ id: r.id, repo: r.repo }] : []));
1713
2513
  const { facts, projected, newRecords, newFlows, looseFact } = factsRecords({
@@ -1757,20 +2557,46 @@ function catalogWithFacts(input) {
1757
2557
  const remap = /* @__PURE__ */ new Map();
1758
2558
  const stepTo = /* @__PURE__ */ new Map();
1759
2559
  let join10;
2560
+ let answerMisses;
2561
+ let estateJoin;
2562
+ let terms;
1760
2563
  let records0 = [...kept2, ...newRecords];
1761
2564
  if (input.estate) {
1762
2565
  const repos = joinReposOf(op, [{ repo, facts }, ...others], renames);
1763
2566
  const whole = joinFacts({
1764
2567
  repos,
1765
2568
  targets: { ...input.estateTargets, [repo]: input.targets ?? {} },
1766
- outside: outsideSystems(op)
2569
+ pairs: { ...input.estatePairs, [repo]: input.pairs ?? {} },
2570
+ aiTargets: { ...input.estateAiTargets, [repo]: input.aiTargets ?? {} },
2571
+ outside: outsideSystems(op),
2572
+ ...joinConventions(op),
2573
+ // G4: the walk through gateways is a stack helper (ATTICUS_HELPERS_OFF=gateway-walk).
2574
+ walkGateways: helperOn("gateway-walk")
1767
2575
  });
1768
2576
  join10 = joinAbout(whole, repo);
2577
+ estateJoin = { whole, repos };
2578
+ const named2 = { ...input.estateTargets, [repo]: input.targets ?? {} };
2579
+ const mine2 = /* @__PURE__ */ new Set([
2580
+ factIdPart(repo),
2581
+ ...repos.filter((r) => r.repo === repo).flatMap((r) => [r.component, ...(r.modules ?? []).map((m) => m.component)]).filter((x) => !!x).map(factIdPart),
2582
+ ...facts.flatMap((f) => f.component ? [factIdPart(f.component)] : [])
2583
+ ]);
2584
+ answerMisses = whole.misses.filter((m) => {
2585
+ const t = named2[m.call.repo]?.[m.call.fact];
2586
+ return !!t && t !== NO_TARGET && (m.call.repo === repo || mine2.has(factIdPart(m.target)));
2587
+ });
1769
2588
  records0 = applyFactJoins(records0, join10, repos, {
1770
- callers: joinCallers({ repos, join: whole, declared: declaredCallsOf(op.profiles) }),
2589
+ callers: joinCallers({
2590
+ repos,
2591
+ join: whole,
2592
+ declared: declaredCallsOf(op.profiles),
2593
+ ...joinConventions(op)
2594
+ }),
1771
2595
  join: whole,
1772
2596
  only: [repo]
1773
2597
  });
2598
+ terms = joinTerms({ repos, join: whole, bundleFolders: joinConventions(op).bundleFolders });
2599
+ records0 = [...records0, ...termGrantRecords(terms, repos, { only: [repo] })];
1774
2600
  for (const j of join10.joins) {
1775
2601
  const id = j.call.repo === repo ? projected.ids[j.call.fact] : void 0;
1776
2602
  if (!id || remap.has(id))
@@ -1779,7 +2605,10 @@ function catalogWithFacts(input) {
1779
2605
  stepTo.set(id, { component: j.entry.component, entry: entryText(j) });
1780
2606
  }
1781
2607
  }
1782
- const merged0 = mergeEstate(op, estateRepos(op), records0.sort(byKindId));
2608
+ const merged0 = mergeEstate(op, estateRepos(op), records0.sort(byKindId), void 0, /* @__PURE__ */ new Set([
2609
+ repo,
2610
+ ...input.estate ? others.filter((r) => r.facts.length > 0).map((r) => r.repo) : []
2611
+ ]));
1783
2612
  const heard = new Set((join10?.joins ?? []).filter((j) => j.how === "topic").map((j) => `${j.entry.repo}|${j.entry.at}|${j.entry.component}`));
1784
2613
  const merged = {
1785
2614
  ...merged0,
@@ -1806,15 +2635,7 @@ function catalogWithFacts(input) {
1806
2635
  joinedTo.set(id, iface.to);
1807
2636
  }
1808
2637
  }
1809
- const re = (id) => {
1810
- const exact = remap.get(id);
1811
- if (exact)
1812
- return exact;
1813
- for (const [from, to] of remap)
1814
- if (id.startsWith(`${from}.`))
1815
- return to + id.slice(from.length);
1816
- return id;
1817
- };
2638
+ const re = remapIds(remap);
1818
2639
  const records = merged.records.map((r) => {
1819
2640
  if (r.kind === "Schema")
1820
2641
  return { ...r, id: re(r.id), ref: re(r.ref) };
@@ -1824,7 +2645,25 @@ function catalogWithFacts(input) {
1824
2645
  return { ...r, interface: re(r.interface) };
1825
2646
  return r;
1826
2647
  });
1827
- const applied = applyCurated([...records].sort(byKindId), input.decisions);
2648
+ const curated = applyCurated([...records].sort(byKindId), input.decisions);
2649
+ const linkEvidence = estateJoin ? linkEvidenceOf({
2650
+ join: estateJoin.whole,
2651
+ repos: estateJoin.repos,
2652
+ contracts: input.linkEvidence?.contracts ?? [],
2653
+ traffic: input.linkEvidence?.traffic ?? []
2654
+ }) : void 0;
2655
+ const evidenced = linkEvidence ? { ...curated, records: applyLinkEvidence(curated.records, linkEvidence.marks) } : curated;
2656
+ const systems = outsideProfileSystems(op);
2657
+ const sharedWhole = estateJoin && input.shared?.length ? joinSharedMaps({
2658
+ outside: estateJoin.whole.outside ?? [],
2659
+ repos: estateJoin.repos,
2660
+ maps: input.shared,
2661
+ systems
2662
+ }) : void 0;
2663
+ const applied = {
2664
+ ...evidenced,
2665
+ records: sharedWhole ? applySharedMaps(evidenced.records, sharedWhole, systems) : stripSharedMaps(evidenced.records)
2666
+ };
1828
2667
  const newIds = new Set(applied.records.filter((r) => r.evidence.some(mine)).map((r) => r.id));
1829
2668
  const oldIds = new Set([...dropped].map((k) => k.slice(k.indexOf("|") + 1)));
1830
2669
  const flows = newFlows.map((f) => {
@@ -1852,10 +2691,10 @@ function catalogWithFacts(input) {
1852
2691
  raw: r.raw
1853
2692
  });
1854
2693
  }
1855
- unresolved.sort((a, b) => a.factId.localeCompare(b.factId));
2694
+ unresolved.sort((a, b) => cmpText(a.factId, b.factId));
1856
2695
  return {
1857
2696
  records: applied.records.sort(byKindId),
1858
- flows: flows.sort((a, b) => a.id.localeCompare(b.id)),
2697
+ flows: flows.sort((a, b) => cmpText(a.id, b.id)),
1859
2698
  unresolved,
1860
2699
  curated: {
1861
2700
  applied: applied.applied,
@@ -1865,7 +2704,11 @@ function catalogWithFacts(input) {
1865
2704
  projectionIssues: projected.issues.filter((i) => i.code !== "unresolved-target"),
1866
2705
  invalid: merged.invalid,
1867
2706
  notes: merged.notes,
1868
- ...join10 ? { join: join10 } : {}
2707
+ ...join10 ? { join: join10 } : {},
2708
+ ...linkEvidence ? { linkEvidence } : {},
2709
+ ...sharedWhole ? { shared: sharedJoinAbout(sharedWhole, repo) } : {},
2710
+ ...terms ? { terms } : {},
2711
+ ...answerMisses?.length ? { answerMisses } : {}
1869
2712
  };
1870
2713
  }
1871
2714
  function prototypeLeftOut(input) {
@@ -1888,16 +2731,99 @@ function prototypeLeftOut(input) {
1888
2731
  ]
1889
2732
  };
1890
2733
  }
2734
+ function estateFromFacts(input) {
2735
+ const repos = input.repos.filter((x) => !isPrototypeRepo(input.op, x.repo));
2736
+ let records = [...input.existing ?? []];
2737
+ const specs = [];
2738
+ const invalid = [];
2739
+ for (const { repo, facts } of repos) {
2740
+ const out = catalogWithFacts({
2741
+ repo,
2742
+ facts,
2743
+ existing: records,
2744
+ op: input.op,
2745
+ decisions: input.decisions ?? [],
2746
+ estate: repos.filter((x) => x.repo !== repo)
2747
+ });
2748
+ records = out.records;
2749
+ specs.push({ repo, flows: out.flows });
2750
+ invalid.push(...out.invalid);
2751
+ }
2752
+ const renames = componentRenames(repos, input.op);
2753
+ const join10 = joinFacts({
2754
+ repos: joinReposOf(input.op, repos.map(({ repo, facts }) => ({
2755
+ repo,
2756
+ facts: factsRecords({ repo, facts, op: input.op, renames }).facts
2757
+ }))),
2758
+ outside: outsideSystems(input.op),
2759
+ ...joinConventions(input.op),
2760
+ walkGateways: helperOn("gateway-walk")
2761
+ });
2762
+ return { records, specs, join: join10, invalid };
2763
+ }
2764
+ function answerRuleOffers(input) {
2765
+ const ruled = input.questions.filter((q) => q.askedBy?.step === TARGETS_STEP && q.state === "answered" && !!q.answer?.match);
2766
+ const groups = /* @__PURE__ */ new Map();
2767
+ for (const r of input.repos)
2768
+ for (const f of r.facts) {
2769
+ if (f.kind !== "call")
2770
+ continue;
2771
+ const q = answeredQuestionOfCall(r.repo, r.facts, input.questions, f.id);
2772
+ const label2 = q ? answerLabel2(q) : void 0;
2773
+ if (!q || !label2 || label2 === TARGETS_NONE || label2 === TARGETS_SPLIT || label2 === TARGETS_ANOTHER)
2774
+ continue;
2775
+ const url = urlOf(f, r.facts);
2776
+ const host = url ? urlHostPort(url)?.host : void 0;
2777
+ const via = host ? `on ${host.toLowerCase()}` : f.base ? `through ${f.base}` : void 0;
2778
+ if (!via)
2779
+ continue;
2780
+ groups.set(via, [
2781
+ ...groups.get(via) ?? [],
2782
+ { repo: r.repo, q, label: label2, f, facts: r.facts, ...r.type ? { type: r.type } : {} }
2783
+ ]);
2784
+ }
2785
+ const out = [];
2786
+ for (const [via, xs] of [...groups].sort(([a], [b]) => cmpText(a, b))) {
2787
+ const repos = [...new Set(xs.map((x) => x.repo))].sort();
2788
+ const labels = new Set(xs.map((x) => slugify(x.label)));
2789
+ if (repos.length < 2 || labels.size !== 1)
2790
+ continue;
2791
+ if (xs.every((x) => ruled.some((q) => ruleCoversCall(q.answer.match, x.f, x.facts))))
2792
+ continue;
2793
+ const label2 = xs[0].label;
2794
+ const prefixes = new Set(xs.map((x) => targetPathPrefix(x.f.path)));
2795
+ const prefix = prefixes.size === 1 ? [...prefixes][0] : void 0;
2796
+ const match = via.startsWith("on ") ? via.slice(3) : prefix && prefix !== "/" ? `${prefix}/**` : void 0;
2797
+ const types = new Set(xs.map((x) => x.type));
2798
+ const type = types.size === 1 ? xs[0].type : void 0;
2799
+ const questions = [...new Set(xs.map((x) => x.q.id))].sort();
2800
+ const first = xs.find((x) => x.q.id === questions[0]).q;
2801
+ const n = first.options.findIndex((o) => o.label === label2);
2802
+ const pick = n >= 0 ? `--option ${n + 1}` : `--text "${label2}"`;
2803
+ if (!match || !type)
2804
+ continue;
2805
+ const command = `atticus questions answer ${first.id} ${pick} --match "${match}" --scope repo-type --type ${type}`;
2806
+ out.push({
2807
+ via,
2808
+ label: label2,
2809
+ repos,
2810
+ questions,
2811
+ match,
2812
+ type,
2813
+ command,
2814
+ line: `${repos.length} repos (${repos.join(", ")}) answer the calls ${via} the same way, ${label2} (${questions.join(", ")}): one answer can say it for every ${type} repo. As a rule: ${command}.`
2815
+ });
2816
+ }
2817
+ return out;
2818
+ }
1891
2819
 
1892
2820
  // packages/scanner/dist/facts-scan.js
1893
- import { readFile as readFile4 } from "node:fs/promises";
1894
- import { join as join6 } from "node:path";
1895
2821
  async function factsForScan(root, op, repo, commit, readerRecords, at3 = {}) {
1896
2822
  const rel = `${factsRelOf(root)}/${repo}.jsonl`;
1897
- const text = await readFile4(join6(factsDirOf(root), `${repo}.jsonl`), "utf8").catch(() => void 0);
1898
- if (text === void 0 || text.trim() === "")
2823
+ const text2 = await readFile4(join6(factsDirOf(root), `${repo}.jsonl`), "utf8").catch(() => void 0);
2824
+ if (text2 === void 0 || text2.trim() === "")
1899
2825
  return {};
1900
- const parsed = parseFacts(text);
2826
+ const parsed = parseFacts(text2);
1901
2827
  if (!parsed.ok)
1902
2828
  return {
1903
2829
  note: `${repo}: ${rel} doesn't read as facts (${parsed.issues[0]?.message ?? "invalid"}); its readers' records are used. Run atticus verify ${repo}.`
@@ -1910,29 +2836,45 @@ async function factsForScan(root, op, repo, commit, readerRecords, at3 = {}) {
1910
2836
  };
1911
2837
  const older = commit ? facts.filter((f) => !sameCommit(f.commit, commit)) : [];
1912
2838
  let carriedSaid = "";
2839
+ let unread = [];
1913
2840
  if (older.length) {
1914
2841
  const diff = at3.dir ? await changesSince(at3.dir, older.map((f) => f.commit), commit, at3.git) : void 0;
1915
2842
  if (!diff || diff.missing.length)
1916
2843
  return {
1917
2844
  note: `${repo}: its facts are of commit ${(diff?.missing[0] ?? older[0].commit).slice(0, 7)}, the checkout is at ${commit.slice(0, 7)}, and the two can't be compared here; its readers' records are used until it is read again (/atticus-map ${repo}, then atticus verify ${repo} --write).`
1918
2845
  };
2846
+ const off = stackHelpersOff();
1919
2847
  const report = await verifyFacts({
1920
2848
  facts,
1921
2849
  source: dirSource(at3.dir),
2850
+ ...off.has("derived-names") ? { derivedNames: false } : {},
2851
+ ...off.has("readers") ? { readers: false } : {},
1922
2852
  commit,
1923
- carry: diff.changes
2853
+ carry: diff.changes,
2854
+ conventions: conventionsOf(op),
2855
+ // ADR 0180: a type taken out of the organisation's set after a read is flagged here too.
2856
+ componentTypes: new Set(effectiveComponentTypes(op.componentTypes).map((t) => t.id))
1924
2857
  });
1925
2858
  const from = [...new Set(older.map((f) => f.commit.slice(0, 7)))].join(", ");
1926
2859
  if (!report.accepted.length)
1927
2860
  return {
1928
2861
  note: `${repo}: every one of its facts (of commit ${from}) cites a file changed since, or names a fact that does; its readers' records are used until it is read again (/atticus-map ${repo}, then atticus verify ${repo} --write).`
1929
2862
  };
2863
+ unread = rereadPlan({
2864
+ facts,
2865
+ commit,
2866
+ carry: diff.changes,
2867
+ source: dirSource(at3.dir),
2868
+ conventions: conventionsOf(op)
2869
+ }).files.map((f) => f.path);
1930
2870
  facts = report.accepted;
1931
2871
  const left = report.rejected.length;
1932
2872
  carriedSaid = `; ${report.carried.length} carried from commit ${from} to ${commit.slice(0, 7)} (the files they cite are unchanged, their lines checked again)${left ? `, ${left} left out until read again (they cite a file changed since, or name a fact that does: /atticus-map ${repo}, then atticus verify ${repo} --write)` : ""}`;
1933
2873
  }
1934
- const questions = (await listQuestions(root, { repo }).catch(() => ({ questions: [] }))).questions;
1935
- const targets = targetsFromAnswers(repo, facts, questions);
2874
+ const questions = (await listQuestions(root, { step: TARGETS_STEP }).catch(() => ({ questions: [] }))).questions;
2875
+ const targets = targetsFromAnswers(repo, facts, questions, {
2876
+ repoType: estateRepos(op).find((r) => r.id === repo)?.type
2877
+ });
1936
2878
  const others = (await estateFacts(root, op, repo)).repos;
1937
2879
  const renames = componentRenames([{ repo, facts }, ...others], op);
1938
2880
  const { newRecords, facts: used } = factsRecords({ repo, facts, op, targets, renames });
@@ -1958,40 +2900,57 @@ async function factsForScan(root, op, repo, commit, readerRecords, at3 = {}) {
1958
2900
  continue;
1959
2901
  extra.set(r.kind, (extra.get(r.kind) ?? 0) + 1);
1960
2902
  }
1961
- const said = [...extra.entries()].sort((a, b) => a[0].localeCompare(b[0])).map(([k, n]) => `${n} ${k}`).join(", ");
2903
+ const said = [...extra.entries()].sort((a, b) => cmpText(a[0], b[0])).map(([k, n]) => `${n} ${k}`).join(", ");
1962
2904
  return {
1963
2905
  records: newRecords,
1964
2906
  facts: used,
1965
2907
  targets,
2908
+ basis: { from: "facts", unread },
1966
2909
  note: `${repo}: read from its facts (${facts.length}, ${rel}${carriedSaid})${said ? `; its readers found ${said} the facts don't have (a cross-check: read those lines again, or a reader misreads them)` : "; its readers agree"}.`
1967
2910
  };
1968
2911
  }
2912
+ async function pairsOfRepo(root, repo, facts) {
2913
+ const text2 = await readFile4(join6(factsDirOf(root), `${repo}.corrections.jsonl`), "utf8").catch(() => void 0);
2914
+ if (!text2 || !/"(?:un)?link"/.test(text2))
2915
+ return {};
2916
+ return pairsFromCorrections(facts, parseCorrections(text2).corrections, { declined: true });
2917
+ }
1969
2918
  async function estateFacts(root, op, except) {
1970
2919
  const repos = [];
1971
2920
  const targets = {};
2921
+ const aiTargets = {};
2922
+ const pairs = {};
1972
2923
  const notes = [];
2924
+ const unverified = [];
2925
+ const questions = (await listQuestions(root, { step: TARGETS_STEP }).catch(() => ({ questions: [] }))).questions;
1973
2926
  for (const r of productionRepos(op)) {
1974
2927
  if (r.id === except)
1975
2928
  continue;
1976
- const text = await readFile4(join6(factsDirOf(root), `${r.id}.jsonl`), "utf8").catch(() => void 0);
1977
- if (text === void 0 || text.trim() === "")
2929
+ const text2 = await readFile4(join6(factsDirOf(root), `${r.id}.jsonl`), "utf8").catch(() => void 0);
2930
+ if (text2 === void 0 || text2.trim() === "")
1978
2931
  continue;
1979
- const parsed = parseFacts(text);
2932
+ const parsed = parseFacts(text2);
1980
2933
  if (!parsed.ok) {
1981
2934
  notes.push(`${r.id}: its facts don't read, so the estate join leaves them out.`);
1982
2935
  continue;
1983
2936
  }
1984
2937
  if (parsed.facts.some((f) => f.verified === "none")) {
2938
+ unverified.push(r.id);
1985
2939
  notes.push(`${r.id}: some of its facts aren't verified yet, so the estate join leaves them out (atticus verify ${r.id}).`);
1986
2940
  continue;
1987
2941
  }
1988
2942
  repos.push({ repo: r.id, facts: parsed.facts });
1989
- const questions = (await listQuestions(root, { repo: r.id }).catch(() => ({ questions: [] }))).questions;
1990
- const named2 = targetsFromAnswers(r.id, parsed.facts, questions);
2943
+ const ai = {};
2944
+ const named2 = targetsFromAnswers(r.id, parsed.facts, questions, { repoType: r.type, ai });
1991
2945
  if (Object.keys(named2).length)
1992
2946
  targets[r.id] = named2;
2947
+ if (Object.keys(ai).length)
2948
+ aiTargets[r.id] = ai;
2949
+ const paired = await pairsOfRepo(root, r.id, parsed.facts);
2950
+ if (Object.keys(paired).length)
2951
+ pairs[r.id] = paired;
1993
2952
  }
1994
- return { repos, targets, notes };
2953
+ return { repos, targets, pairs, notes, unverified, aiTargets };
1995
2954
  }
1996
2955
 
1997
2956
  // packages/scanner/dist/scan-settings.js
@@ -2006,10 +2965,10 @@ async function citedLine(repo, at3) {
2006
2965
  const i = at3.lastIndexOf(":");
2007
2966
  const path = at3.slice(0, i);
2008
2967
  const n = Number(at3.slice(i + 1));
2009
- const text = await repo.readText(path).catch(() => void 0);
2010
- if (text === void 0)
2968
+ const text2 = await repo.readText(path).catch(() => void 0);
2969
+ if (text2 === void 0)
2011
2970
  return void 0;
2012
- return text.replace(/\r\n?/g, "\n").split("\n")[n - 1];
2971
+ return text2.replace(/\r\n?/g, "\n").split("\n")[n - 1];
2013
2972
  }
2014
2973
  var CODE_EXT = new Set("java kt scala groovy ts tsx js jsx mjs cjs py go rb cs fs vb php rs swift m mm c cc cpp h hpp dart ex exs erl clj lua pl".split(" "));
2015
2974
  var NOT_OWN = /(^|\/)(node_modules|\.git|target|bin|obj|dist|build|\.atticus|vendor)\//;
@@ -2068,8 +3027,8 @@ async function checkScanSettings(s, repo) {
2068
3027
  if (!(isConfigFile(file) && names)) {
2069
3028
  const dropped = code.filter((f) => re.test(f));
2070
3029
  const root = mainRoots.find((d) => {
2071
- const under = code.filter((f) => f.startsWith(`${d}/`));
2072
- return under.length && under.every((f) => re.test(f));
3030
+ const under2 = code.filter((f) => f.startsWith(`${d}/`));
3031
+ return under2.length && under2.every((f) => re.test(f));
2073
3032
  });
2074
3033
  const why = " so it is still read; only a build or config file that leaves it out itself can say so";
2075
3034
  if (dropped.length * 2 > code.length) {
@@ -2126,9 +3085,9 @@ async function namesNotIn(repo, paths, names) {
2126
3085
  for await (const f of repo.listFiles(["**/*"])) {
2127
3086
  if (!paths.some((x) => x.test(f)))
2128
3087
  continue;
2129
- const text = await repo.readText(f).catch(() => "");
3088
+ const text2 = await repo.readText(f).catch(() => "");
2130
3089
  for (const n of [...left])
2131
- if (text.includes(n))
3090
+ if (text2.includes(n))
2132
3091
  left.delete(n);
2133
3092
  if (left.size === 0)
2134
3093
  break;
@@ -2194,6 +3153,12 @@ function withExcludes(repo, exclude) {
2194
3153
  yield f;
2195
3154
  }
2196
3155
  });
3156
+ const listing = /* @__PURE__ */ Symbol.for("@maycombhq/sdk.listing");
3157
+ const before = repo[listing];
3158
+ Object.defineProperty(wrapped, listing, {
3159
+ value: `${typeof before === "string" ? before : ""}|exclude:${JSON.stringify(exclude.map((x) => `${x.source}/${x.flags}`))}`,
3160
+ configurable: true
3161
+ });
2197
3162
  return wrapped;
2198
3163
  }
2199
3164
  var join7 = (prefix, path) => `${prefix.replace(/\/+$/, "")}${path.startsWith("/") ? path : `/${path}`}`.replace(/\/+$/, "") || "/";
@@ -2355,54 +3320,6 @@ function withPluginScan(scan, extra) {
2355
3320
  };
2356
3321
  }
2357
3322
 
2358
- // packages/scanner/dist/scan.js
2359
- import { stat } from "node:fs/promises";
2360
- import { dirname as dirname3, isAbsolute, join as join9, resolve as resolve2 } from "node:path";
2361
- import { fileURLToPath } from "node:url";
2362
-
2363
- // packages/scanner/dist/federate-loose.js
2364
- function resolveFederatedLooseEnds(o) {
2365
- const originOf = (r) => (r.flags ?? []).find((f) => f.startsWith("federated:"))?.slice("federated:".length);
2366
- const loose = o.federated.filter((r) => r.kind === "Unresolved");
2367
- if (!loose.length)
2368
- return { federated: o.federated, resolved: /* @__PURE__ */ new Map() };
2369
- const fedComponents = o.federated.filter((r) => r.kind === "Component");
2370
- const result = o.merge([...o.raw, ...fedComponents, ...loose]);
2371
- const have = new Set(o.local.map((r) => `${r.kind}/${r.id}`));
2372
- const known = /* @__PURE__ */ new Set([
2373
- ...o.local.filter((r) => r.kind === "Component" || r.kind === "System").map((r) => r.id),
2374
- ...fedComponents.map((r) => r.id)
2375
- ]);
2376
- const origin = new Map(fedComponents.map((c) => [c.id, originOf(c)]));
2377
- const stillLoose = new Set(result.records.filter((r) => r.kind === "Unresolved").map((r) => r.id));
2378
- const added = [];
2379
- for (const r of result.records) {
2380
- if (r.kind !== "Interface" || have.has(`Interface/${r.id}`))
2381
- continue;
2382
- const i = r;
2383
- const from = origin.get(i.from) ?? origin.get(i.to);
2384
- if (!from || !known.has(i.from) || !known.has(i.to))
2385
- continue;
2386
- added.push({ ...r, flags: [.../* @__PURE__ */ new Set([...r.flags ?? [], `federated:${from}`])] });
2387
- }
2388
- const key = (e) => JSON.stringify(e);
2389
- const carried = new Set(added.flatMap((i) => i.evidence.map(key)));
2390
- const resolved = /* @__PURE__ */ new Map();
2391
- const kept2 = o.federated.filter((r) => {
2392
- if (r.kind !== "Unresolved" || stillLoose.has(r.id))
2393
- return true;
2394
- if (!r.evidence.some((e) => e.source !== "export" && carried.has(key(e))))
2395
- return true;
2396
- const id = originOf(r) ?? "";
2397
- resolved.set(id, (resolved.get(id) ?? 0) + 1);
2398
- return false;
2399
- });
2400
- return {
2401
- federated: [...kept2, ...added].sort((a, b) => a.kind === b.kind ? a.id.localeCompare(b.id) : a.kind.localeCompare(b.kind)),
2402
- resolved
2403
- };
2404
- }
2405
-
2406
3323
  // packages/scanner/dist/scan.js
2407
3324
  var STRUCTURED = /\.(ya?ml|json|xml|tf|tfvars|properties|toml|conf|hcl)$/i;
2408
3325
  var TOOLING = /(^|\/)(package(-lock)?\.json|pnpm-lock\.yaml|yarn\.lock|tsconfig[^/]*\.json|\.eslintrc[^/]*|\.prettierrc[^/]*|renovate\.json|\.vscode\/.*|\.idea\/.*|pom\.xml|catalog-info\.yaml|mkdocs\.yml|codecov\.yml)$/i;
@@ -2418,13 +3335,6 @@ async function orgPackDirFor(root) {
2418
3335
  return { dir: join9(root, ".atticus", "org-pack"), adopted: true };
2419
3336
  return { dir: join9(root, "org-pack"), adopted: false };
2420
3337
  }
2421
- function inPlace(root, url) {
2422
- if (url === "." || url === "./")
2423
- return root;
2424
- if (url.startsWith("./") && !url.split("/").includes(".."))
2425
- return join9(root, url.slice(2));
2426
- return void 0;
2427
- }
2428
3338
  async function federate(local, sources, problems, root) {
2429
3339
  const have = new Set(local.map((r) => `${r.kind}/${r.id}`));
2430
3340
  const records = [];
@@ -2553,7 +3463,8 @@ async function scanEstate(o) {
2553
3463
  }));
2554
3464
  const brokenDialects = /* @__PURE__ */ new Set();
2555
3465
  const dialects = await usableDialects(op, orgPackDir, problems, brokenDialects);
2556
- const frameworks = await usableFrameworks(op, problems);
3466
+ const off = stackHelpersOff();
3467
+ const frameworks = off.has("framework-models") ? [] : await usableFrameworks(op, problems);
2557
3468
  const pluginSet = await usablePlugins(op);
2558
3469
  problems.push(...pluginSet.problems);
2559
3470
  const byName = new Map(dialects.map((d) => [d.name, d]));
@@ -2563,6 +3474,7 @@ async function scanEstate(o) {
2563
3474
  const repos = productionRepos(op, o.repos);
2564
3475
  const results = [];
2565
3476
  const factNotes = prototypeScanNotes(op, o.repos);
3477
+ const basis = {};
2566
3478
  const factRepos = [];
2567
3479
  const structuredOf = /* @__PURE__ */ new Map();
2568
3480
  const modulesOf = /* @__PURE__ */ new Map();
@@ -2570,7 +3482,7 @@ async function scanEstate(o) {
2570
3482
  let done = 0;
2571
3483
  for (const r of repos) {
2572
3484
  throwIfCancelled();
2573
- const dir = o.repoDir?.(r.id) ?? inPlace(o.root, r.url) ?? baseClonePath(o.root, r.id);
3485
+ const dir = o.repoDir?.(r.id) ?? inPlaceDir(o.root, r.url) ?? baseClonePath(o.root, r.id);
2574
3486
  let state;
2575
3487
  if (!o.repoState && !await stat(dir).catch(() => void 0)) {
2576
3488
  problems.push({
@@ -2590,7 +3502,7 @@ async function scanEstate(o) {
2590
3502
  });
2591
3503
  continue;
2592
3504
  }
2593
- const extractors = [
3505
+ const extractors = off.has("readers") ? [] : [
2594
3506
  ...BUILT_IN_EXTRACTORS,
2595
3507
  ...o.extractors ?? [],
2596
3508
  ...dialectExtractors.filter(({ d }) => r.dialects.includes(d.name) || d.appliesTo.repoTypes.includes(r.type) || // "*": any repo where the marker matches (ADR 0062).
@@ -2670,6 +3582,7 @@ async function scanEstate(o) {
2670
3582
  });
2671
3583
  if (fromFacts.note)
2672
3584
  factNotes.push(fromFacts.note);
3585
+ basis[r.id] = fromFacts.records && fromFacts.basis ? fromFacts.basis : { from: "readers" };
2673
3586
  if (fromFacts.facts)
2674
3587
  factRepos.push({ repo: r.id, facts: fromFacts.facts, targets: fromFacts.targets ?? {} });
2675
3588
  results.push({ ...scan, records: fromFacts.records ?? readerRecords });
@@ -2682,10 +3595,17 @@ async function scanEstate(o) {
2682
3595
  const join10 = joinFacts({
2683
3596
  repos: joinRepos,
2684
3597
  targets: Object.fromEntries(factRepos.map((x) => [x.repo, x.targets])),
2685
- outside: outsideSystems(op)
3598
+ outside: outsideSystems(op),
3599
+ ...joinConventions(op),
3600
+ walkGateways: helperOn("gateway-walk")
2686
3601
  });
2687
3602
  codeRecords = applyFactJoins(codeRecords, join10, joinRepos, {
2688
- callers: joinCallers({ repos: joinRepos, join: join10, declared: declaredCallsOf(op.profiles) }),
3603
+ callers: joinCallers({
3604
+ repos: joinRepos,
3605
+ join: join10,
3606
+ declared: declaredCallsOf(op.profiles),
3607
+ ...joinConventions(op)
3608
+ }),
2689
3609
  join: join10
2690
3610
  });
2691
3611
  }
@@ -2739,7 +3659,7 @@ async function scanEstate(o) {
2739
3659
  orgPackDir
2740
3660
  });
2741
3661
  const notes = [...factNotes, ...merged.notes, ...glossary.problems.map((p) => `glossary: ${p}`)];
2742
- const withFeatures = [...glossary.records, ...fedRecords].sort((a, b) => a.kind === b.kind ? a.id.localeCompare(b.id) : a.kind.localeCompare(b.kind));
3662
+ const withFeatures = [...glossary.records, ...fedRecords].sort((a, b) => a.kind === b.kind ? cmpText(a.id, b.id) : cmpText(a.kind, b.kind));
2743
3663
  const applied = applyCurated(withFeatures, curated.records);
2744
3664
  for (const p of applied.problems)
2745
3665
  problems.push({ source: "curated", message: JSON.stringify(p) });
@@ -2800,7 +3720,8 @@ async function scanEstate(o) {
2800
3720
  ...inputs ? { inputs } : {},
2801
3721
  curated: { applied: applied.applied, suppressed: applied.suppressed },
2802
3722
  notes,
2803
- ...federated ? { federated } : {}
3723
+ ...federated ? { federated } : {},
3724
+ basis
2804
3725
  };
2805
3726
  }
2806
3727
 
@@ -2836,8 +3757,8 @@ var RepoFiles = class {
2836
3757
  }
2837
3758
  async linesOf(path) {
2838
3759
  if (!this.lines.has(path)) {
2839
- const text = await this.repo.readText(path).catch(() => void 0);
2840
- this.lines.set(path, text === void 0 ? null : text.replace(/\r\n?/g, "\n").split("\n"));
3760
+ const text2 = await this.repo.readText(path).catch(() => void 0);
3761
+ this.lines.set(path, text2 === void 0 ? null : text2.replace(/\r\n?/g, "\n").split("\n"));
2841
3762
  }
2842
3763
  return this.lines.get(path);
2843
3764
  }
@@ -2913,13 +3834,13 @@ async function scoreType(t, files) {
2913
3834
  };
2914
3835
  }
2915
3836
  function typeFromWords(description, types) {
2916
- const text = (description ?? "").toLowerCase();
2917
- if (!text)
3837
+ const text2 = (description ?? "").toLowerCase();
3838
+ if (!text2)
2918
3839
  return void 0;
2919
3840
  const hits = [];
2920
3841
  for (const t of types)
2921
3842
  for (const words2 of [t.name.toLowerCase(), t.id.replace(/[-_.]+/g, " ")])
2922
- if (words2.length > 2 && new RegExp(`(^|[^a-z0-9])${escapeRe(words2)}([^a-z0-9]|$)`).test(text))
3843
+ if (words2.length > 2 && new RegExp(`(^|[^a-z0-9])${escapeRe(words2)}([^a-z0-9]|$)`).test(text2))
2923
3844
  hits.push({ type: t.id, words: words2 });
2924
3845
  const ids = new Set(hits.map((h) => h.type));
2925
3846
  if (ids.size !== 1)
@@ -2932,7 +3853,7 @@ async function matchRepoType(repo, types, o = {}) {
2932
3853
  for (const t of types)
2933
3854
  matches.push(await scoreType(t, files));
2934
3855
  const pct = (m) => Math.round(m.score * 100);
2935
- matches.sort((a, b) => Number(a.ruledOut) - Number(b.ruledOut) || pct(b) - pct(a) || a.type.localeCompare(b.type));
3856
+ matches.sort((a, b) => Number(a.ruledOut) - Number(b.ruledOut) || pct(b) - pct(a) || cmpText(a.type, b.type));
2936
3857
  const said = typeFromWords(o.description, types);
2937
3858
  const eligible = matches.filter((m) => !m.ruledOut && pct(m) >= PARTIAL_MATCH * 100);
2938
3859
  const top2 = eligible[0];
@@ -2975,8 +3896,8 @@ async function readRule(rule, path, files) {
2975
3896
  const key = rule.key;
2976
3897
  if (/\.ya?ml$/i.test(path)) {
2977
3898
  const counter = new import_yaml3.LineCounter();
2978
- const text = lines.join("\n");
2979
- for (const doc of (0, import_yaml3.parseAllDocuments)(text, { lineCounter: counter })) {
3899
+ const text2 = lines.join("\n");
3900
+ for (const doc of (0, import_yaml3.parseAllDocuments)(text2, { lineCounter: counter })) {
2980
3901
  if (!("getIn" in doc))
2981
3902
  continue;
2982
3903
  for (const node of [doc.getIn(key.split("."), true), doc.get(key, true)]) {
@@ -3015,8 +3936,8 @@ async function resolveSettingsTemplate(t, repo, repoId) {
3015
3936
  out.exclude.push({ path: e.path, why: e.why, at: `${first}:1` });
3016
3937
  }
3017
3938
  for (const rule of t.settings.prefixes) {
3018
- const candidates = (await files.matching(rule.files)).sort((a, b) => a.length - b.length || a.localeCompare(b));
3019
- for (const f of candidates) {
3939
+ const candidates2 = (await files.matching(rule.files)).sort((a, b) => a.length - b.length || cmpText(a, b));
3940
+ for (const f of candidates2) {
3020
3941
  const hit = await readRule(rule, f, files);
3021
3942
  if (!hit)
3022
3943
  continue;
@@ -3049,15 +3970,15 @@ var BUILD_FILES = [
3049
3970
  ["composer.json", "composer.json"]
3050
3971
  ].map(([name, glob]) => ({ name, glob, re: globToRegExp(`**/${glob}`) }));
3051
3972
  var PACKAGE_NAME = /^[A-Za-z0-9_.@/-]+$/;
3052
- function declaredDependencies(path, text) {
3973
+ function declaredDependencies(path, text2) {
3053
3974
  const out = [];
3054
3975
  const all = (re, g = 1) => {
3055
- for (const m of text.matchAll(re))
3976
+ for (const m of text2.matchAll(re))
3056
3977
  if (m[g])
3057
3978
  out.push(m[g].trim());
3058
3979
  };
3059
3980
  if (path.endsWith("pom.xml")) {
3060
- for (const block2 of text.matchAll(/<(parent|dependency|plugin)>([\s\S]*?)<\/\1>/g)) {
3981
+ for (const block2 of text2.matchAll(/<(parent|dependency|plugin)>([\s\S]*?)<\/\1>/g)) {
3061
3982
  const a = /<artifactId>\s*([^<\s]+)\s*<\/artifactId>/.exec(block2[2]);
3062
3983
  if (a)
3063
3984
  out.push(a[1]);
@@ -3066,7 +3987,7 @@ function declaredDependencies(path, text) {
3066
3987
  all(/['"]([\w.-]+):([\w.-]+)(?::[^'"]*)?['"]/g, 2);
3067
3988
  } else if (path.endsWith("package.json")) {
3068
3989
  try {
3069
- const j = JSON.parse(text);
3990
+ const j = JSON.parse(text2);
3070
3991
  const deps = Object.keys(j.dependencies ?? {});
3071
3992
  out.push(...deps.length ? deps : Object.keys(j.devDependencies ?? {}));
3072
3993
  } catch {
@@ -3090,7 +4011,7 @@ var PLAIN_ANNOTATIONS = /* @__PURE__ */ new Set([
3090
4011
  "JvmStatic"
3091
4012
  ]);
3092
4013
  function top(counts, n) {
3093
- return [...counts].sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0])).slice(0, n).map(([k]) => k);
4014
+ return [...counts].sort((a, b) => b[1] - a[1] || cmpText(a[0], b[0])).slice(0, n).map(([k]) => k);
3094
4015
  }
3095
4016
  var TEST_FILE = /(^|\/)(test|tests|__tests__|src\/test)\/|\.(spec|test)\.[jt]sx?$|Tests?\.(java|kt|cs)$/;
3096
4017
  async function draftMarkers(files) {
@@ -3176,9 +4097,9 @@ async function draftRepoType(repo, id) {
3176
4097
  fingerprint.push({ kind: "build-file", globs: buildGlobs, weight: 2, required: true });
3177
4098
  const deps = /* @__PURE__ */ new Map();
3178
4099
  for (const f of builds) {
3179
- const text = (await files.linesOf(f) ?? []).join("\n");
4100
+ const text2 = (await files.linesOf(f) ?? []).join("\n");
3180
4101
  const glob = buildGlobs.find((g) => globToRegExp(g).test(f));
3181
- for (const d of declaredDependencies(f, text)) {
4102
+ for (const d of declaredDependencies(f, text2)) {
3182
4103
  if (!PACKAGE_NAME.test(d) || d === repo.name)
3183
4104
  continue;
3184
4105
  if (!deps.has(d))
@@ -3318,7 +4239,7 @@ function specTriggers(records, repoId, template) {
3318
4239
  }
3319
4240
  }
3320
4241
  }
3321
- return out.sort((a, b) => a.id.localeCompare(b.id));
4242
+ return out.sort((a, b) => cmpText(a.id, b.id));
3322
4243
  }
3323
4244
  var methodPath = (s) => {
3324
4245
  const m = /^([A-Za-z]+)\s+(\S+)$/.exec(s.trim());
@@ -3740,7 +4661,7 @@ function parseEntry(s) {
3740
4661
  const m = /^([A-Za-z]+)\s+(\S+)$/.exec(s.trim());
3741
4662
  return m ? { method: m[1].toUpperCase(), path: m[2] } : { path: s.trim() };
3742
4663
  }
3743
- var segs = (p) => normalizePath(p).toLowerCase().split("/").filter(Boolean);
4664
+ var segs2 = (p) => normalizePath(p).toLowerCase().split("/").filter(Boolean);
3744
4665
  var sameMethod = (a, b) => !a || !b || a.toUpperCase() === b.toUpperCase();
3745
4666
  async function joinEstate(o) {
3746
4667
  const byId = new Map(o.records.map((r) => [r.id, r]));
@@ -3752,8 +4673,8 @@ async function joinEstate(o) {
3752
4673
  const out = [i.normalizedPath];
3753
4674
  const route = i.route ? byId.get(i.route) : void 0;
3754
4675
  if (route?.kind === "GatewayRoute") {
3755
- const base2 = segs(route.basepath);
3756
- const p = segs(i.normalizedPath);
4676
+ const base2 = segs2(route.basepath);
4677
+ const p = segs2(i.normalizedPath);
3757
4678
  if (base2.length && base2.every((s, k) => p[k] === s))
3758
4679
  out.push(`/${p.slice(base2.length).join("/")}`);
3759
4680
  }
@@ -3789,7 +4710,7 @@ async function joinEstate(o) {
3789
4710
  const c = byId.get(id);
3790
4711
  return c?.kind === "Component" ? c.repo ?? c.evidence.find((e) => e.repo)?.repo : void 0;
3791
4712
  };
3792
- const servingExactly = (caller, want, scope) => endpoints.filter((e) => e.component !== caller && (!scope || scope.has(e.component)) && sameMethod(want.method, e.method) && pathScore(want.path, e.normalizedPath ?? e.pathOrTopic) === 1e3).sort((a, b) => a.id.localeCompare(b.id));
4713
+ const servingExactly = (caller, want, scope) => endpoints.filter((e) => e.component !== caller && (!scope || scope.has(e.component)) && sameMethod(want.method, e.method) && pathScore(want.path, e.normalizedPath ?? e.pathOrTopic) === 1e3).sort((a, b) => cmpText(a.id, b.id));
3793
4714
  const joins = [];
3794
4715
  const unmatched = [];
3795
4716
  const inferred = [];
@@ -3868,27 +4789,27 @@ async function joinEstate(o) {
3868
4789
  within = "estate";
3869
4790
  hits = servingExactly(flow.component, want);
3870
4791
  }
3871
- const candidates = [...new Set(hits.map((e) => e.component))].sort();
3872
- if (candidates.length === 0) {
4792
+ const candidates2 = [...new Set(hits.map((e) => e.component))].sort();
4793
+ if (candidates2.length === 0) {
3873
4794
  miss({
3874
4795
  reason: "no-target",
3875
4796
  message: `${s.to.component} isn't a component in the catalog, and neither an Interface from ${flow.component} nor an entry point of the estate serves ${s.to.entry}`
3876
4797
  });
3877
4798
  continue;
3878
4799
  }
3879
- if (candidates.length > 1) {
3880
- const repos = [...new Set(candidates.map(repoOfComponent))];
4800
+ if (candidates2.length > 1) {
4801
+ const repos = [...new Set(candidates2.map(repoOfComponent))];
3881
4802
  const of = repos.length === 1 && repos[0] ? repos[0] : "the estate";
3882
4803
  miss({
3883
4804
  reason: "no-target",
3884
- message: `${s.to.component} isn't a component in the catalog, and ${candidates.length} components of ${of} serve ${s.to.entry} (${candidates.join(", ")}): a person says which`
4805
+ message: `${s.to.component} isn't a component in the catalog, and ${candidates2.length} components of ${of} serve ${s.to.entry} (${candidates2.join(", ")}): a person says which`
3885
4806
  });
3886
4807
  asks.push({
3887
4808
  from,
3888
4809
  caller: flow.component,
3889
4810
  named: s.to.component,
3890
4811
  what: s.to.entry,
3891
- candidates
4812
+ candidates: candidates2
3892
4813
  });
3893
4814
  continue;
3894
4815
  }
@@ -3961,7 +4882,7 @@ async function joinEstate(o) {
3961
4882
  inferred.push({ from: { repo, flow: flow.id, step }, named: s.to.component, ...guess });
3962
4883
  }
3963
4884
  }
3964
- const order = (a, b) => a.from.flow.localeCompare(b.from.flow) || a.from.step - b.from.step;
4885
+ const order = (a, b) => cmpText(a.from.flow, b.from.flow) || a.from.step - b.from.step;
3965
4886
  return {
3966
4887
  joins: joins.sort(order),
3967
4888
  unmatched: unmatched.sort(order),
@@ -4116,6 +5037,17 @@ function fieldLineage(f, field) {
4116
5037
  }
4117
5038
 
4118
5039
  // packages/scanner/dist/hosts-asked.js
5040
+ var segs3 = (p) => p.split(/[?#]/)[0].split("/").filter(Boolean).map((x) => /^[{:<$]/.test(x) ? "{}" : x);
5041
+ var underPath = (path, base2) => {
5042
+ const b = segs3(base2);
5043
+ const c = segs3(path);
5044
+ return b.length > 0 && b.length <= c.length && b.every((x, i) => x === c[i]);
5045
+ };
5046
+ var servesPath = (entry, path) => {
5047
+ const e = segs3(entry);
5048
+ const c = segs3(path);
5049
+ return e.length === c.length && e.every((x, i) => x === "{}" || c[i] === "{}" || x === c[i]);
5050
+ };
4119
5051
  var HTTP_TYPES = /* @__PURE__ */ new Set(["rest", "graphql", "soap", "websocket", "sse", "webhook"]);
4120
5052
  var CONFIG_FILES = [
4121
5053
  "**/application*.properties",
@@ -4178,8 +5110,12 @@ async function unknownHosts(o) {
4178
5110
  const entry = byHost.get(h.label) ?? {
4179
5111
  ...h.port !== void 0 ? { port: h.port } : {},
4180
5112
  explicit: h.label !== h.host,
4181
- calls: []
5113
+ calls: [],
5114
+ paths: /* @__PURE__ */ new Set()
4182
5115
  };
5116
+ const path = /^[a-z][a-z0-9+.-]*:\/\/[^/?#]*(\/[^?#]*)?/i.exec(url)?.[1];
5117
+ if (path)
5118
+ entry.paths.add(path);
4183
5119
  const at3 = `${e.path}:${e.line}`;
4184
5120
  if (!entry.calls.some((c) => c.repo === repo && c.at === at3))
4185
5121
  entry.calls.push({ repo, at: at3 });
@@ -4217,7 +5153,7 @@ async function unknownHosts(o) {
4217
5153
  return out;
4218
5154
  };
4219
5155
  const asks = [];
4220
- for (const [host, entry] of [...byHost.entries()].sort(([a], [b]) => a.localeCompare(b))) {
5156
+ for (const [host, entry] of [...byHost.entries()].sort(([a], [b]) => cmpText(a, b))) {
4221
5157
  if (servers.length === 0)
4222
5158
  continue;
4223
5159
  const hits = [];
@@ -4231,7 +5167,26 @@ async function unknownHosts(o) {
4231
5167
  }
4232
5168
  }
4233
5169
  const suggest = hits.length === 1 ? hits[0] : void 0;
4234
- const calls = entry.calls.sort((a, b) => a.repo.localeCompare(b.repo) || a.at.localeCompare(b.at, "en", { numeric: true }));
5170
+ const calls = entry.calls.sort((a, b) => cmpText(a.repo, b.repo) || cmpNatural(a.at, b.at));
5171
+ const byPath = [];
5172
+ for (const g of o.records) {
5173
+ if (g.kind !== "GatewayRoute")
5174
+ continue;
5175
+ const repo = g.evidence.find((e) => e.repo)?.repo;
5176
+ if (!repo || byPath.some((b) => b.repo === repo))
5177
+ continue;
5178
+ const under2 = [...entry.paths].filter((p) => underPath(p, g.basepath));
5179
+ if (!under2.length)
5180
+ continue;
5181
+ const comps = new Set([...compRepo].filter(([, r]) => r === repo).map(([c]) => c));
5182
+ const hit = o.records.find((r) => r.kind === "Endpoint" && comps.has(r.component) && under2.some((p) => servesPath(r.pathOrTopic, p)));
5183
+ byPath.push({
5184
+ repo,
5185
+ serves: hit ? [hit.method?.toUpperCase(), hit.pathOrTopic].filter(Boolean).join(" ") : `the routes under ${g.basepath}`
5186
+ });
5187
+ }
5188
+ byPath.sort((a, b) => cmpText(a.repo, b.repo));
5189
+ const first = suggest ? [suggest.repo] : byPath.map((b) => b.repo).filter((x) => servers.includes(x));
4235
5190
  const about = Object.fromEntries(servers.flatMap((s) => {
4236
5191
  const d = o.repos.find((r) => r.id === s)?.description;
4237
5192
  return d ? [[s, { text: d.text, trust: d.trust }]] : [];
@@ -4239,9 +5194,10 @@ async function unknownHosts(o) {
4239
5194
  asks.push({
4240
5195
  host,
4241
5196
  calls,
4242
- servers: suggest ? [suggest.repo, ...servers.filter((s) => s !== suggest.repo)] : servers,
5197
+ servers: [...first, ...servers.filter((s) => !first.includes(s))],
4243
5198
  ...suggest ? { suggest } : {},
4244
- ...Object.keys(about).length ? { about } : {}
5199
+ ...Object.keys(about).length ? { about } : {},
5200
+ ...byPath.length ? { byPath } : {}
4245
5201
  });
4246
5202
  }
4247
5203
  return asks;
@@ -4252,23 +5208,38 @@ function aboutOf(a, repo) {
4252
5208
  return "";
4253
5209
  return d.trust === "suggested" ? `. What it is, as your assistant drafted it (unconfirmed): ${d.text}` : `. What it is: ${d.text}`;
4254
5210
  }
5211
+ function gatewayHostFix(host, byPath) {
5212
+ const repos = byPath.map((b) => b.repo);
5213
+ return `${repos.length === 1 ? `${repos[0]}'s routes serve` : `The routes of ${repos.join(", ")} serve`} the paths the calls to ${host} make, and no repo lists ${host}: it looks like a shared gateway's host. Once, not per call: list ${host} under hosts: on each proxy repo in repos.manifest.yaml (${repos.join(", ")}, and any other proxy on it); the path then picks the proxy, and this question retires itself`;
5214
+ }
4255
5215
  function hostQuestionDrafts(asks) {
4256
5216
  return asks.map((a) => {
4257
5217
  const repo = a.calls[0].repo;
5218
+ const byPath = new Map((a.byPath ?? []).map((b) => [b.repo, b.serves]));
4258
5219
  return {
4259
5220
  repo,
5221
+ ...a.byPath?.length ? { byPath: a.byPath } : {},
4260
5222
  draft: {
4261
5223
  question: hostQuestionText(a.host),
4262
5224
  options: [
4263
5225
  ...a.servers.map((s) => ({
4264
5226
  label: s,
4265
- means: (a.suggest?.repo === s ? `${s} serves HTTP, and its ${a.suggest.at} sets the port ${a.host.split(":").pop()} (${a.suggest.line})` : `${s} serves HTTP`) + aboutOf(a, s),
4266
- changes: `${a.host} goes under hosts for ${s} in repos.manifest.yaml, and the calls to it join ${s} on the next scan`
5227
+ means: (a.suggest?.repo === s ? `${s} serves HTTP, and its ${a.suggest.at} sets the port ${a.host.split(":").pop()} (${a.suggest.line})` : byPath.has(s) ? `${s} serves ${byPath.get(s)}, a path the calls to ${a.host} make` : `${s} serves HTTP`) + aboutOf(a, s),
5228
+ changes: byPath.has(s) ? `${a.host} goes under hosts for ${s} in repos.manifest.yaml, and the calls to it join ${s} on the next scan; on a shared gateway host, list it on each proxy repo instead (${[...byPath.keys()].join(", ")}, and any other proxy on it), and the path picks the proxy` : `${a.host} goes under hosts for ${s} in repos.manifest.yaml, and the calls to it join ${s} on the next scan`
5229
+ })),
5230
+ // O1: a system outside the estate, declared in the profile with the host as its alias.
5231
+ ...[
5232
+ [HOSTS_OUTSIDE_INTERNAL, "internal", "another team's system"],
5233
+ [HOSTS_OUTSIDE_VENDOR, "vendor", "a vendor's system"]
5234
+ ].map(([label2, boundary, what2]) => ({
5235
+ label: label2,
5236
+ means: `${a.host} is ${what2}, outside this estate, that the org pack doesn't name yet`,
5237
+ changes: `A system ${outsideSystemOf(a.host).id} (boundary ${boundary}, alias ${a.host}) goes into the profile, and the calls to ${a.host} go to it on the next scan`
4267
5238
  })),
4268
5239
  {
4269
5240
  label: HOSTS_NONE,
4270
- means: `An outside system answers on ${a.host}, not a repo of this estate`,
4271
- changes: "Nothing is added to the manifest; the calls stay loose ends, and this isn't asked again"
5241
+ means: `Neither a repo of this estate nor a system to add to the org pack answers on ${a.host}`,
5242
+ changes: "Nothing is added to the manifest or the profile; the calls stay loose ends, and this isn't asked again"
4272
5243
  }
4273
5244
  ],
4274
5245
  evidence: a.calls.filter((c) => c.repo === repo).slice(0, 3).map((c) => c.at),
@@ -4278,6 +5249,375 @@ function hostQuestionDrafts(asks) {
4278
5249
  });
4279
5250
  }
4280
5251
 
5252
+ // packages/scanner/dist/measure-links.js
5253
+ var import_yaml4 = __toESM(require_dist(), 1);
5254
+ var METHODS = /^(GET|POST|PUT|PATCH|DELETE|HEAD|OPTIONS|TRACE|ANY)\s+(\S.*)$/i;
5255
+ var NEAR = 3;
5256
+ var AT = /^.+:\d+$/;
5257
+ var AT_HELP = "at is file:line, e.g. src/main/java/Client.java:16";
5258
+ var isObj2 = (x) => typeof x === "object" && x !== null && !Array.isArray(x);
5259
+ var text = (x) => typeof x === "string" && x.trim() ? x.trim() : typeof x === "number" ? String(x) : void 0;
5260
+ function parseLinkReference(source) {
5261
+ let raw;
5262
+ try {
5263
+ raw = (0, import_yaml4.parse)(source);
5264
+ } catch (e) {
5265
+ return { ok: false, issues: [`the file isn't YAML: ${e.message}`] };
5266
+ }
5267
+ const issues = [];
5268
+ const say = (where, message) => issues.push(`${where}: ${message}`);
5269
+ const only = (where, o, keys) => {
5270
+ for (const k of Object.keys(o))
5271
+ if (!keys.includes(k))
5272
+ say(where, `unknown key ${k}`);
5273
+ };
5274
+ if (!isObj2(raw) || !Array.isArray(raw.repos))
5275
+ return { ok: false, issues: ["repos: list the repos, each with its entries and calls"] };
5276
+ only("(top)", raw, ["estate", "repos"]);
5277
+ const repos = [];
5278
+ raw.repos.forEach((r, i) => {
5279
+ const w = `repos.${i}`;
5280
+ if (!isObj2(r) || !text(r.repo))
5281
+ return say(w, "each repo needs repo: <its id>");
5282
+ only(w, r, ["repo", "entries", "calls"]);
5283
+ const entries = [];
5284
+ const calls = [];
5285
+ const list = (x, name) => {
5286
+ if (x === void 0 || x === null)
5287
+ return [];
5288
+ if (Array.isArray(x))
5289
+ return x;
5290
+ say(`${w}.${name}`, `${name} is a list`);
5291
+ return [];
5292
+ };
5293
+ list(r.entries, "entries").forEach((e, j) => {
5294
+ const we = `${w}.entries.${j}`;
5295
+ if (text(e))
5296
+ return entries.push({ entry: text(e) });
5297
+ if (!isObj2(e))
5298
+ return say(we, "an entry is METHOD /path, or { entry, at, fact }");
5299
+ only(we, e, ["entry", "at", "fact"]);
5300
+ const at3 = text(e.at);
5301
+ if (at3 && !AT.test(at3))
5302
+ return say(`${we}.at`, AT_HELP);
5303
+ if (!text(e.entry) && !at3 && !text(e.fact))
5304
+ return say(we, "give the entry (METHOD /path), its line (at: file:line) or its fact id");
5305
+ entries.push(stripped({ entry: text(e.entry), at: at3, fact: text(e.fact) }));
5306
+ });
5307
+ list(r.calls, "calls").forEach((c, j) => {
5308
+ const wc = `${w}.calls.${j}`;
5309
+ if (!isObj2(c))
5310
+ return say(wc, "a call is { call, at, reaches } or { call, at, outside }");
5311
+ only(wc, c, ["call", "at", "fact", "reaches", "outside", "note"]);
5312
+ const reaches = text(c.reaches);
5313
+ const outside = text(c.outside);
5314
+ const at3 = text(c.at);
5315
+ let bad = false;
5316
+ if (reaches && outside) {
5317
+ say(wc, "give one of reaches (a repo, or none) and outside (a system), not both");
5318
+ bad = true;
5319
+ } else if (!reaches && !outside) {
5320
+ say(wc, "give one of reaches (a repo, or none) and outside (a system)");
5321
+ bad = true;
5322
+ }
5323
+ if (!text(c.call) && !at3 && !text(c.fact)) {
5324
+ say(wc, "give the call (METHOD /path), its line (at: file:line) or its fact id");
5325
+ bad = true;
5326
+ }
5327
+ if (at3 && !AT.test(at3)) {
5328
+ say(`${wc}.at`, AT_HELP);
5329
+ bad = true;
5330
+ }
5331
+ if (bad)
5332
+ return;
5333
+ const want = outside ? { outside } : reaches.toLowerCase() === "none" ? { none: true } : { repo: reaches };
5334
+ calls.push({
5335
+ ...stripped({ call: text(c.call), at: at3, fact: text(c.fact) }),
5336
+ want
5337
+ });
5338
+ });
5339
+ repos.push({ repo: text(r.repo), entries, calls });
5340
+ });
5341
+ if (issues.length)
5342
+ return { ok: false, issues };
5343
+ const estate = text(raw.estate);
5344
+ return { ok: true, reference: { ...estate ? { estate } : {}, repos } };
5345
+ }
5346
+ var stripped = (o) => Object.fromEntries(Object.entries(o).filter(([, v]) => v !== void 0));
5347
+ function segs4(path) {
5348
+ return path.split(/[?#]/)[0].split("/").filter(Boolean).map((s) => /^(\{[^}]*\}|:\w+|\*)$/.test(s) ? "{}" : s.toLowerCase());
5349
+ }
5350
+ function pathMatch(a, b) {
5351
+ const x = segs4(a);
5352
+ const y = segs4(b);
5353
+ if (x.length === y.length && x.every((s, i) => s === y[i]))
5354
+ return 2;
5355
+ const [short, long] = x.length < y.length ? [x, y] : [y, x];
5356
+ if (!short.length)
5357
+ return 0;
5358
+ const off = long.length - short.length;
5359
+ return short.every((s, i) => s === long[off + i]) ? 1 : 0;
5360
+ }
5361
+ function parseWhat(what2) {
5362
+ if (!what2)
5363
+ return {};
5364
+ const m = METHODS.exec(what2.trim());
5365
+ return m ? { method: m[1].toUpperCase(), path: m[2].trim() } : { path: what2.trim() };
5366
+ }
5367
+ function parseAt(at3) {
5368
+ if (!at3)
5369
+ return void 0;
5370
+ const i = at3.lastIndexOf(":");
5371
+ return { file: at3.slice(0, i).replace(/^\.\//, ""), line: Number(at3.slice(i + 1)) };
5372
+ }
5373
+ function finder(facts) {
5374
+ const used = /* @__PURE__ */ new Set();
5375
+ return (ref) => {
5376
+ const { method, path } = parseWhat(ref.what);
5377
+ const at3 = parseAt(ref.at);
5378
+ let best;
5379
+ for (const f of facts) {
5380
+ if (used.has(f.id))
5381
+ continue;
5382
+ if (ref.fact) {
5383
+ if (f.id === ref.fact)
5384
+ best = { f, score: 1e6 };
5385
+ continue;
5386
+ }
5387
+ let score = 0;
5388
+ if (at3) {
5389
+ const d = Math.abs(f.cite.line - at3.line);
5390
+ if (f.cite.file !== at3.file || d > NEAR)
5391
+ continue;
5392
+ score += 10 * (NEAR + 1 - d);
5393
+ }
5394
+ if (method && f.method && f.method.toUpperCase() !== method && method !== "ANY")
5395
+ continue;
5396
+ if (path) {
5397
+ const p = pathMatch(path, f.path);
5398
+ if (!p)
5399
+ continue;
5400
+ score += 100 * p;
5401
+ }
5402
+ if (!best || score > best.score)
5403
+ best = { f, score };
5404
+ }
5405
+ if (best)
5406
+ used.add(best.f.id);
5407
+ return best?.f;
5408
+ };
5409
+ }
5410
+ var round = (n, d) => d ? Math.round(n / d * 1e3) / 1e3 : null;
5411
+ var what = (f) => [f.method?.toUpperCase(), f.path].filter(Boolean).join(" ");
5412
+ function measureLinks(input) {
5413
+ const { op } = input;
5414
+ const repos = input.repos.filter((x) => !isPrototypeRepo(op, x.repo));
5415
+ const renames = componentRenames(repos, op);
5416
+ const join10 = joinFacts({
5417
+ repos: joinReposOf(op, repos, renames),
5418
+ ...input.targets ? { targets: input.targets } : {},
5419
+ outside: outsideSystems(op),
5420
+ ...joinConventions(op),
5421
+ walkGateways: input.walkGateways ?? helperOn("gateway-walk")
5422
+ });
5423
+ const key = (repo, fact) => `${repo}\0${fact}`;
5424
+ const joins = /* @__PURE__ */ new Map();
5425
+ for (const j of join10.joins) {
5426
+ const k = key(j.call.repo, j.call.fact);
5427
+ joins.set(k, [...joins.get(k) ?? [], j]);
5428
+ }
5429
+ const asks = new Map(join10.asks.map((a) => [key(a.call.repo, a.call.fact), a]));
5430
+ const misses = new Map(join10.misses.map((m) => [key(m.call.repo, m.call.fact), m]));
5431
+ const outsideOf = new Map((join10.outside ?? []).map((o) => [key(o.call.repo, o.call.fact), o]));
5432
+ const own = new Set(estateRepos(op).flatMap((r) => r.system ? [r.system] : []));
5433
+ const systemOf = /* @__PURE__ */ new Map();
5434
+ for (const s of op.profiles.flatMap((p) => p.profile?.systems ?? []))
5435
+ if (s.boundary === "vendor" || !own.has(s.id))
5436
+ for (const n of [s.id, s.name, ...s.aliases])
5437
+ systemOf.set(factIdPart(n), s.id);
5438
+ const outsideName = (n) => n ? systemOf.get(factIdPart(n)) : void 0;
5439
+ const manifests = estateRepos(op);
5440
+ const repoOf = (name) => {
5441
+ const slug = factIdPart(name);
5442
+ const m = manifests.find((r) => r.id === name || factIdPart(r.id) === slug) ?? manifests.find((r) => r.component && factIdPart(r.component) === slug);
5443
+ return m?.id ?? name;
5444
+ };
5445
+ const factsOf = new Map(repos.map((r) => [r.repo, r.facts]));
5446
+ const notes = [];
5447
+ const reading = {
5448
+ entries: { want: 0, found: 0 },
5449
+ calls: { want: 0, found: 0 },
5450
+ missed: []
5451
+ };
5452
+ const rows = [];
5453
+ for (const r of input.reference.repos) {
5454
+ const facts = factsOf.get(r.repo);
5455
+ if (!facts)
5456
+ notes.push(`${r.repo}: no verified facts in the estate, so nothing of it is found (atticus map ${r.repo}, then atticus verify ${r.repo}).`);
5457
+ const all = facts ?? [];
5458
+ const findEntry = finder(all.filter((f) => f.kind === "entry"));
5459
+ for (const e of r.entries) {
5460
+ reading.entries.want++;
5461
+ if (findEntry({ what: e.entry, at: e.at, fact: e.fact }))
5462
+ reading.entries.found++;
5463
+ else
5464
+ reading.missed.push(`${r.repo} entry ${e.entry ?? e.fact ?? e.at}`);
5465
+ }
5466
+ const findCall = finder(all.filter((f) => f.kind === "call"));
5467
+ for (const c of r.calls) {
5468
+ reading.calls.want++;
5469
+ const f = findCall({ what: c.call, at: c.at, fact: c.fact });
5470
+ const label2 = c.call ?? (f ? what(f) : c.fact ?? c.at);
5471
+ const base2 = { repo: r.repo, call: label2, ...c.at ? { at: c.at } : {} };
5472
+ if (!f) {
5473
+ reading.missed.push(`${r.repo} call ${label2}`);
5474
+ rows.push({ ...base2, want: c.want, got: { kind: "not-read" }, right: false });
5475
+ continue;
5476
+ }
5477
+ reading.calls.found++;
5478
+ const got = gotOf(r.repo, f, all);
5479
+ rows.push({ ...base2, fact: f.id, want: c.want, got, right: isRight(c.want, got) });
5480
+ }
5481
+ }
5482
+ function gotOf(repo, f, facts) {
5483
+ const k = key(repo, f.id);
5484
+ const j = joins.get(k)?.[0];
5485
+ if (j) {
5486
+ const via = (j.gateway ?? []).map((h) => h.entry.repo);
5487
+ const inferred2 = joinGuessesOf(j)[0];
5488
+ return {
5489
+ kind: "joined",
5490
+ repo: j.entry.repo,
5491
+ component: j.entry.component,
5492
+ entry: [j.method, j.path].filter(Boolean).join(" "),
5493
+ ...via.length ? { via } : {},
5494
+ ...inferred2 ? { inferred: inferred2 } : {}
5495
+ };
5496
+ }
5497
+ const ask = asks.get(k);
5498
+ if (ask)
5499
+ return { kind: "asked", reason: ask.reason };
5500
+ const miss = misses.get(k);
5501
+ if (miss)
5502
+ return { kind: "missed", target: miss.target };
5503
+ const answer = input.targets?.[repo]?.[f.id];
5504
+ if (answer === NO_TARGET)
5505
+ return { kind: "unlinked" };
5506
+ const host = callHostOf(f, facts);
5507
+ const system = outsideName(answer) ?? outsideName(outsideOf.get(k)?.target) ?? outsideName(f.to) ?? outsideName(host);
5508
+ if (system)
5509
+ return { kind: "outside", system };
5510
+ if (f.to && host)
5511
+ return { kind: "outside", system: f.to };
5512
+ return { kind: "unlinked" };
5513
+ }
5514
+ function isRight(want, got) {
5515
+ if ("repo" in want) {
5516
+ if (got.kind !== "joined")
5517
+ return false;
5518
+ const w = repoOf(want.repo);
5519
+ return got.repo === w || got.component === factIdPart(want.repo);
5520
+ }
5521
+ if ("outside" in want) {
5522
+ if (got.kind !== "outside")
5523
+ return false;
5524
+ const w = outsideName(want.outside) ?? factIdPart(want.outside);
5525
+ return got.system === w || factIdPart(got.system) === factIdPart(want.outside);
5526
+ }
5527
+ return got.kind === "unlinked" || got.kind === "missed";
5528
+ }
5529
+ const toRepo = rows.filter((x) => "repo" in x.want);
5530
+ const joined = rows.filter((x) => x.got.kind === "joined");
5531
+ const right = joined.filter((x) => x.right).length;
5532
+ const inferred = joined.filter((x) => x.got.kind === "joined" && x.got.inferred);
5533
+ const outsideWant = rows.filter((x) => "outside" in x.want);
5534
+ const noneWant = rows.filter((x) => "none" in x.want);
5535
+ return {
5536
+ ...input.reference.estate ? { estate: input.reference.estate } : {},
5537
+ reading,
5538
+ links: {
5539
+ want: toRepo.length,
5540
+ joined: joined.length,
5541
+ right,
5542
+ precision: round(right, joined.length),
5543
+ recall: round(toRepo.filter((x) => x.right).length, toRepo.length),
5544
+ asked: rows.filter((x) => x.got.kind === "asked").length,
5545
+ missed: rows.filter((x) => x.got.kind === "missed").length,
5546
+ unlinked: rows.filter((x) => x.got.kind === "unlinked").length,
5547
+ notRead: rows.filter((x) => x.got.kind === "not-read").length,
5548
+ inferred: { joined: inferred.length, right: inferred.filter((x) => x.right).length },
5549
+ outside: {
5550
+ want: outsideWant.length,
5551
+ got: rows.filter((x) => x.got.kind === "outside").length,
5552
+ right: outsideWant.filter((x) => x.right).length
5553
+ },
5554
+ none: { want: noneWant.length, right: noneWant.filter((x) => x.right).length }
5555
+ },
5556
+ calls: rows,
5557
+ notes
5558
+ };
5559
+ }
5560
+
5561
+ // packages/scanner/dist/audit-links.js
5562
+ function estateJoinOf(op, list, targets, pairs, aiTargets) {
5563
+ const repos = list.filter((x) => !isPrototypeRepo(op, x.repo));
5564
+ const renames = componentRenames(repos, op);
5565
+ return joinFacts({
5566
+ repos: joinReposOf(op, repos, renames),
5567
+ ...targets ? { targets } : {},
5568
+ ...aiTargets && Object.keys(aiTargets).length ? { aiTargets } : {},
5569
+ ...pairs && Object.keys(pairs).length ? { pairs } : {},
5570
+ outside: outsideSystems(op),
5571
+ // The org pack's facing words and gateway bundle folders (ADR 0147), as the catalog joins.
5572
+ ...joinConventions(op),
5573
+ walkGateways: helperOn("gateway-walk")
5574
+ });
5575
+ }
5576
+ function auditLinkCandidates(join10, list, repo) {
5577
+ const facts = /* @__PURE__ */ new Map();
5578
+ for (const r of list)
5579
+ for (const f of r.facts)
5580
+ facts.set(`${r.repo}\0${f.id}`, f);
5581
+ const mine = join10.joins.filter((j) => j.call.repo === repo);
5582
+ const perCall = /* @__PURE__ */ new Map();
5583
+ for (const j of mine)
5584
+ perCall.set(j.call.fact, (perCall.get(j.call.fact) ?? 0) + 1);
5585
+ const at3 = (e) => `${e.repo} ${e.at}`;
5586
+ const out = [];
5587
+ for (const j of mine) {
5588
+ const call = facts.get(`${j.call.repo}\0${j.call.fact}`);
5589
+ const said = [call?.method?.toUpperCase(), call?.path].filter(Boolean).join(" ");
5590
+ const via = (j.gateway ?? []).map((h) => h.entry.repo);
5591
+ const flags = joinFlagsOf(j);
5592
+ const cites = [
5593
+ at3(j.call),
5594
+ ...j.tie ? [at3(j.tie)] : [],
5595
+ ...(j.through ?? []).map(at3),
5596
+ ...(j.exposedBy ?? []).map(at3),
5597
+ ...(j.gateway ?? []).flatMap((h) => [
5598
+ at3(h.entry),
5599
+ at3(h.call),
5600
+ ...h.tie ? [at3(h.tie)] : [],
5601
+ ...(h.through ?? []).map(at3),
5602
+ ...(h.exposedBy ?? []).map(at3)
5603
+ ]),
5604
+ ...(j.entryThrough ?? []).map(at3),
5605
+ at3(j.entry)
5606
+ ];
5607
+ out.push({
5608
+ id: `link:${j.call.fact}${(perCall.get(j.call.fact) ?? 0) > 1 ? `~${j.entry.fact}` : ""}`,
5609
+ kind: "link",
5610
+ // A publish joined to its listener carries no flag: its topic says how (ADR 0135).
5611
+ of: flags[0] ?? (j.how === "topic" ? "topic" : `joined-by-${j.how}`),
5612
+ claim: `${repo} ${said || j.call.fact} -> ${j.entry.repo} ${[j.method, j.path].filter(Boolean).join(" ")}${via.length ? ` via ${via.join(", ")}` : ""}`,
5613
+ cites: [...new Set(cites)],
5614
+ fact: j.call.fact,
5615
+ to: j.entry.repo
5616
+ });
5617
+ }
5618
+ return out.sort((a, b) => a.id < b.id ? -1 : a.id > b.id ? 1 : 0);
5619
+ }
5620
+
4281
5621
  export {
4282
5622
  buildGlossary,
4283
5623
  GLOSSARY_CHANGE,
@@ -4300,31 +5640,100 @@ export {
4300
5640
  diffCatalogs,
4301
5641
  diffIsEmpty,
4302
5642
  estateRepos,
5643
+ productionRepos,
5644
+ prototypeScanNotes,
4303
5645
  isPrototypeRepo,
5646
+ estateMergeInput,
5647
+ mergeEstate,
5648
+ curatedDirOf,
5649
+ TARGETS_MAX_SUGGESTED,
5650
+ TARGETS_ANOTHER,
5651
+ TARGETS_NO_EVIDENCE,
5652
+ nameWords,
5653
+ targetSuggestions,
5654
+ offeredSuggestions,
5655
+ targetPicker,
5656
+ pickerLines,
5657
+ topicQuestionDrafts,
4304
5658
  TARGETS_STEP,
5659
+ TARGETS_NONE,
5660
+ remapIds,
5661
+ targetQuestionFor,
5662
+ suggestionWhy,
4305
5663
  targetQuestionDrafts,
5664
+ TARGETS_SPLIT,
5665
+ targetPathPrefix,
5666
+ targetGroupQuestion,
5667
+ targetGroupWordings,
5668
+ splitTargetGroups,
5669
+ groupedTargetDrafts,
5670
+ targetRuleOffers,
5671
+ ruleCoversCall,
5672
+ ruledTargets,
4306
5673
  targetsFromAnswers,
5674
+ answeredQuestionOfCall,
5675
+ answerReconfirmed,
5676
+ staleTargetQuestions,
5677
+ staleTopicQuestions,
5678
+ componentRenames,
5679
+ factsRecords,
4307
5680
  outsideSystems,
5681
+ outsideProfileSystems,
5682
+ hostAliases,
4308
5683
  joinReposOf,
5684
+ applyFactJoins,
4309
5685
  catalogWithFacts,
4310
5686
  prototypeLeftOut,
5687
+ estateFromFacts,
5688
+ answerRuleOffers,
5689
+ factsForScan,
5690
+ pairsOfRepo,
4311
5691
  estateFacts,
5692
+ NO_SETTINGS,
4312
5693
  citedLine,
5694
+ isCodeFile,
5695
+ isConfigFile,
5696
+ globName,
5697
+ prefixOf,
4313
5698
  checkScanSettings,
5699
+ joinBase,
5700
+ applyClients,
5701
+ withExcludes,
5702
+ applyPrefixes,
5703
+ pluginExtractor,
4314
5704
  runPluginTests,
5705
+ pluginApplies,
5706
+ usablePlugins,
5707
+ withPluginScan,
5708
+ orgPackDirFor,
4315
5709
  scanEstate,
5710
+ CLEAR_MATCH,
5711
+ PARTIAL_MATCH,
5712
+ TIE_MARGIN,
4316
5713
  matchRepoType,
4317
5714
  resolveSettingsTemplate,
4318
5715
  draftRepoType,
4319
5716
  repoComponents,
5717
+ pathScore,
5718
+ resourceKey,
5719
+ familyMismatches,
4320
5720
  specTriggers,
5721
+ lineNamesUiWord,
5722
+ uiFindings,
4321
5723
  checkSpec,
4322
5724
  staleFlows,
4323
5725
  citedFileHashes,
4324
5726
  estateStepKey,
4325
5727
  joinEstate,
4326
5728
  estateFlow,
5729
+ flattenEstateFlow,
4327
5730
  fieldLineage,
5731
+ lineSetsPort,
4328
5732
  unknownHosts,
4329
- hostQuestionDrafts
5733
+ gatewayHostFix,
5734
+ hostQuestionDrafts,
5735
+ parseLinkReference,
5736
+ measureLinks,
5737
+ estateJoinOf,
5738
+ auditLinkCandidates
4330
5739
  };