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

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 (273) hide show
  1. package/dist/bin.js +8 -4
  2. package/dist/chunk-265LWMOA.js +91 -0
  3. package/dist/chunk-26WBY34Y.js +37 -0
  4. package/dist/{chunk-UBBC2XSG.js → chunk-26Z7OGQW.js} +518 -93
  5. package/dist/chunk-2D7PWSOK.js +172 -0
  6. package/dist/{chunk-JCZIKA4C.js → chunk-2HH3UCZM.js} +4 -4
  7. package/dist/{chunk-GDLULHTC.js → chunk-2HTEMYTB.js} +12 -26
  8. package/dist/chunk-2UCRFXFZ.js +130 -0
  9. package/dist/{chunk-MVZVGYXH.js → chunk-2V2ZPQQU.js} +81 -13
  10. package/dist/{chunk-BOHJZTEQ.js → chunk-36RY7ZXM.js} +130 -71
  11. package/dist/{chunk-I3E7CX4H.js → chunk-37WE3LS2.js} +3 -3
  12. package/dist/{chunk-IPKHE5D6.js → chunk-3AQ2R5NL.js} +1520 -871
  13. package/dist/chunk-3G6S7LAA.js +43 -0
  14. package/dist/chunk-3J5I7ZOU.js +58 -0
  15. package/dist/{chunk-65NYPGRC.js → chunk-3LZ6P5EP.js} +1 -1
  16. package/dist/chunk-3SSTNB4E.js +191 -0
  17. package/dist/chunk-3TD6S4TC.js +49 -0
  18. package/dist/{chunk-3C7EYRFZ.js → chunk-3UTYPHN3.js} +1 -1
  19. package/dist/{chunk-G2B4ZQGH.js → chunk-3WON4G3M.js} +2 -2
  20. package/dist/{chunk-S3QNRWYR.js → chunk-42EPP7OB.js} +12 -12
  21. package/dist/chunk-455SYIIE.js +113 -0
  22. package/dist/{chunk-YISXLPHN.js → chunk-4B6BT75O.js} +166 -23
  23. package/dist/{chunk-C6EKFH7J.js → chunk-4CLK2RJO.js} +7 -7
  24. package/dist/chunk-4CYTJ5TZ.js +38 -0
  25. package/dist/{chunk-EYWIDJFU.js → chunk-4D62IZII.js} +5 -5
  26. package/dist/chunk-4FOAYLI4.js +328 -0
  27. package/dist/{chunk-7JUKDRSZ.js → chunk-4KNOJC6J.js} +294 -215
  28. package/dist/{chunk-YMZ3TNYB.js → chunk-4PYC2UGC.js} +2 -2
  29. package/dist/{chunk-27NKUS3X.js → chunk-4SLBCEPD.js} +2104 -726
  30. package/dist/chunk-4TKVU2I2.js +122 -0
  31. package/dist/{chunk-RUVFXE2L.js → chunk-4WXRPQDE.js} +448 -47
  32. package/dist/{chunk-7ZUIMTOU.js → chunk-5CMPVC3N.js} +24 -24
  33. package/dist/chunk-5G7ILBUG.js +66 -0
  34. package/dist/{chunk-HFQCFSJR.js → chunk-5PV7S2ZP.js} +3 -3
  35. package/dist/{chunk-FITWGOJ2.js → chunk-5QUOPNLN.js} +4 -4
  36. package/dist/{chunk-Z6LF7HQN.js → chunk-5TDRBOAV.js} +5 -5
  37. package/dist/{chunk-T6AIP7H5.js → chunk-5WERBKLE.js} +199 -63
  38. package/dist/{chunk-XPCM366L.js → chunk-62DLJSLV.js} +8 -8
  39. package/dist/{chunk-L24C55VN.js → chunk-64AADMTW.js} +99 -25
  40. package/dist/{chunk-K65BJIMT.js → chunk-67BP7HXL.js} +17 -17
  41. package/dist/chunk-6DQYZU2W.js +379 -0
  42. package/dist/{chunk-EDS2L2LD.js → chunk-6EXDI5GI.js} +52 -26
  43. package/dist/chunk-6FNAKCET.js +38 -0
  44. package/dist/{chunk-6IU67AEK.js → chunk-75ASLHBT.js} +21 -15
  45. package/dist/{chunk-A3GY6SOA.js → chunk-75ZOZFGH.js} +18 -18
  46. package/dist/{chunk-MGYZM27B.js → chunk-77O633L5.js} +49 -23
  47. package/dist/{chunk-GQ4N4WKF.js → chunk-7AB6OWTV.js} +3 -3
  48. package/dist/{chunk-M766ZJ6W.js → chunk-7CZHUZBL.js} +25 -25
  49. package/dist/{chunk-A7UPUQFY.js → chunk-7FHNFCHK.js} +643 -124
  50. package/dist/chunk-7FUIVIBG.js +415 -0
  51. package/dist/{chunk-6QSERWXH.js → chunk-7QPCGTPS.js} +21 -21
  52. package/dist/{chunk-LR3R6Z2A.js → chunk-A2G72UI4.js} +7 -7
  53. package/dist/{chunk-I4JRCD6M.js → chunk-AE4P57KK.js} +23 -17
  54. package/dist/{chunk-AVJDSJ2F.js → chunk-AHW3RY3X.js} +7 -7
  55. package/dist/chunk-AM6FP6JW.js +43 -0
  56. package/dist/{chunk-CSZ4C5N6.js → chunk-AOCWFA7T.js} +93 -10
  57. package/dist/{chunk-EPTBZ3GP.js → chunk-API5TOUB.js} +1 -1
  58. package/dist/{chunk-652QWTLZ.js → chunk-AZDITBCT.js} +3 -3
  59. package/dist/{chunk-DPUYDDQG.js → chunk-BA2HUMDN.js} +25 -3
  60. package/dist/chunk-BA3U7ALS.js +99 -0
  61. package/dist/{chunk-HVUPKCGJ.js → chunk-BC6KKZ67.js} +5 -5
  62. package/dist/{chunk-CKSITVS4.js → chunk-BEPCKDCP.js} +2554 -722
  63. package/dist/{chunk-3ALD2W2A.js → chunk-BPPU3PW7.js} +4 -4
  64. package/dist/{chunk-QRAOANID.js → chunk-BVWWPBKD.js} +21 -21
  65. package/dist/{chunk-ZDPAS56H.js → chunk-BXBCMGIQ.js} +7 -1
  66. package/dist/chunk-BXZ4PKIT.js +233 -0
  67. package/dist/chunk-C7CEK3KW.js +247 -0
  68. package/dist/chunk-CFH3QH26.js +29 -0
  69. package/dist/chunk-CG6CULP6.js +170 -0
  70. package/dist/chunk-CLD4RVTA.js +45 -0
  71. package/dist/{chunk-DKJA76CJ.js → chunk-CNPY4NTZ.js} +13 -13
  72. package/dist/chunk-CNU5Q2D5.js +575 -0
  73. package/dist/{chunk-T2UYZSCQ.js → chunk-COYN3UAN.js} +126 -23
  74. package/dist/{chunk-23W7VWKR.js → chunk-CPK7QSBG.js} +1360 -113
  75. package/dist/{chunk-4AK7IBVP.js → chunk-D2WSW4GU.js} +1 -1
  76. package/dist/{chunk-ABWHYWNI.js → chunk-D57MFJPP.js} +3 -3
  77. package/dist/chunk-D5KV5PSK.js +45 -0
  78. package/dist/chunk-D5MASS4I.js +48 -0
  79. package/dist/{chunk-7EOBHUMC.js → chunk-DCWN3HFV.js} +2 -2
  80. package/dist/{chunk-QSXK2RGH.js → chunk-DDJPM5YX.js} +2 -2
  81. package/dist/chunk-DN4A2Q2O.js +57 -0
  82. package/dist/{chunk-IHMLFRVV.js → chunk-DUPZPDCJ.js} +58 -16
  83. package/dist/{chunk-PRPPGYZG.js → chunk-E2VXBSIR.js} +4 -4
  84. package/dist/chunk-E57AOFYN.js +32 -0
  85. package/dist/{chunk-UGLN7QHV.js → chunk-ECLXZWAG.js} +12 -12
  86. package/dist/{chunk-5WG5YP4T.js → chunk-EDZ45WIR.js} +14 -14
  87. package/dist/chunk-EFCQSMXB.js +37 -0
  88. package/dist/{chunk-2ANOROX3.js → chunk-EZIN5E5B.js} +8 -8
  89. package/dist/{chunk-BWZ77RQT.js → chunk-F6H2RJ3N.js} +1 -1
  90. package/dist/{chunk-OLQJTJJU.js → chunk-FAJDKNSD.js} +7 -3
  91. package/dist/chunk-FGAGWYZB.js +47 -0
  92. package/dist/{chunk-4WBVUHXN.js → chunk-FLVZFTLQ.js} +18 -18
  93. package/dist/chunk-FQ6WH6PB.js +140 -0
  94. package/dist/{chunk-T6XQL73F.js → chunk-FRHD22WM.js} +61 -23
  95. package/dist/{chunk-XAKWMCXI.js → chunk-FVMPFCDS.js} +4 -4
  96. package/dist/{chunk-B2WLAWNJ.js → chunk-FY3QIRTL.js} +3 -3
  97. package/dist/{chunk-CPA6HPVR.js → chunk-FYBF5MB2.js} +15 -11
  98. package/dist/{chunk-V2YTAIQO.js → chunk-G5YJBNDZ.js} +21 -6
  99. package/dist/chunk-G6IQYM25.js +339 -0
  100. package/dist/chunk-G6X4YL4D.js +161 -0
  101. package/dist/{chunk-SJRGMG6A.js → chunk-GC2VK3JT.js} +25 -25
  102. package/dist/{chunk-SP2I63OO.js → chunk-GCS3FZOB.js} +77 -3
  103. package/dist/{chunk-6TACQN3N.js → chunk-GDZB57IU.js} +5 -5
  104. package/dist/chunk-GIFMBCTW.js +221 -0
  105. package/dist/{chunk-YY6ZQQBP.js → chunk-GK4PYVEC.js} +18 -8
  106. package/dist/{chunk-TDD55CJH.js → chunk-GNWAOVFQ.js} +6 -5
  107. package/dist/chunk-GS76RXQE.js +226 -0
  108. package/dist/{chunk-YYQN46FY.js → chunk-GSGIZ2OO.js} +23 -23
  109. package/dist/chunk-GUYL7RMK.js +60 -0
  110. package/dist/chunk-GX74YWR7.js +321 -0
  111. package/dist/chunk-HE3HRDNZ.js +122 -0
  112. package/dist/{chunk-AIOQPLA5.js → chunk-HGHRSF3E.js} +30 -12
  113. package/dist/{chunk-2RMYG36Q.js → chunk-HK3VOHIE.js} +198 -336
  114. package/dist/{chunk-QZSX3F7C.js → chunk-HS4BGB4G.js} +24 -22
  115. package/dist/{chunk-XY3P7JW7.js → chunk-HSZMPH5T.js} +834 -149
  116. package/dist/{chunk-L6SGQIR3.js → chunk-HWD4JPU7.js} +4 -2
  117. package/dist/{chunk-PCVDV3UZ.js → chunk-HXXP2NTE.js} +6 -6
  118. package/dist/{chunk-AA6YULYB.js → chunk-I2A3Z5AK.js} +7 -7
  119. package/dist/{chunk-N7IXEHK3.js → chunk-ICHDSWEY.js} +7 -7
  120. package/dist/chunk-IFFRGC3N.js +31 -0
  121. package/dist/{chunk-4BN53TG3.js → chunk-IFHDV6KQ.js} +31 -23
  122. package/dist/chunk-J2O4LFVI.js +80 -0
  123. package/dist/chunk-J5AAZ6OJ.js +161 -0
  124. package/dist/{chunk-ZMFPW66A.js → chunk-JAABT3DA.js} +3 -3
  125. package/dist/chunk-JITJP64K.js +34 -0
  126. package/dist/chunk-JKIGL3XQ.js +162 -0
  127. package/dist/chunk-JRVNXLZP.js +615 -0
  128. package/dist/chunk-JRXHON4E.js +41 -0
  129. package/dist/{chunk-DHPACDG7.js → chunk-JS6WMC3J.js} +8 -8
  130. package/dist/chunk-JU4QNJSR.js +62 -0
  131. package/dist/{chunk-PAMPPUR6.js → chunk-JYMIB5XY.js} +1697 -172
  132. package/dist/{chunk-RRTKKSLQ.js → chunk-JYTIHTIK.js} +8 -6
  133. package/dist/{chunk-3HI6Z55Y.js → chunk-K332XW7K.js} +324 -39
  134. package/dist/{chunk-GLUX4EEP.js → chunk-K6JB6XTX.js} +9 -9
  135. package/dist/chunk-KDAHXQIE.js +294 -0
  136. package/dist/chunk-KYZMRSPS.js +93 -0
  137. package/dist/chunk-L4CESDPB.js +22 -0
  138. package/dist/{chunk-NXFPS2LR.js → chunk-LEBR5NHW.js} +14 -14
  139. package/dist/chunk-LIJJCBQI.js +223 -0
  140. package/dist/{chunk-DE76JROX.js → chunk-LIRVU3HW.js} +16 -16
  141. package/dist/{chunk-JMSCMUCA.js → chunk-LN77DXBM.js} +145 -21
  142. package/dist/{chunk-JIITQXFB.js → chunk-LNWEWYCM.js} +2969 -788
  143. package/dist/{chunk-TANFV3MZ.js → chunk-LO3BLMVG.js} +3 -3
  144. package/dist/chunk-LPV6T362.js +250 -0
  145. package/dist/{chunk-2VHF7OXM.js → chunk-LQH4P45K.js} +1 -1
  146. package/dist/chunk-LU4MQVAL.js +41 -0
  147. package/dist/chunk-LWDNKE4X.js +273 -0
  148. package/dist/chunk-M4H2UG4R.js +218 -0
  149. package/dist/{chunk-6BVONWJ7.js → chunk-M6D2U3YM.js} +3 -3
  150. package/dist/chunk-MD7KUEJ3.js +514 -0
  151. package/dist/{chunk-66YVAGGM.js → chunk-MMZUN7ZO.js} +20 -20
  152. package/dist/chunk-MZGRDONP.js +916 -0
  153. package/dist/chunk-N3RUGTGV.js +39 -0
  154. package/dist/{chunk-JGZCG5VM.js → chunk-NCB6656Z.js} +7 -7
  155. package/dist/{chunk-4NDAAFQT.js → chunk-NRQ65LTQ.js} +7 -5
  156. package/dist/{chunk-UNM6ZOKP.js → chunk-NTAVOWYD.js} +145 -18
  157. package/dist/{chunk-3USYZGRF.js → chunk-NYOABJNK.js} +36 -27
  158. package/dist/{chunk-WVPJSRRJ.js → chunk-O4IKAIZ2.js} +24 -20
  159. package/dist/{chunk-D3PO4DOZ.js → chunk-ODMBPDDV.js} +51 -14
  160. package/dist/chunk-OKUB6RJA.js +533 -0
  161. package/dist/chunk-OMPENNWY.js +39 -0
  162. package/dist/chunk-OPQZISG3.js +25 -0
  163. package/dist/{chunk-5GGOLAKC.js → chunk-OV57AHCG.js} +26 -26
  164. package/dist/chunk-OZJ3JLYI.js +58 -0
  165. package/dist/{chunk-KEM4SDDP.js → chunk-P2VBXX7N.js} +11 -10
  166. package/dist/{chunk-S2A7YC3Q.js → chunk-PAINMSID.js} +10150 -1720
  167. package/dist/chunk-PKJAV25S.js +43 -0
  168. package/dist/{chunk-EBEW3XXQ.js → chunk-PNQLCRSS.js} +4 -4
  169. package/dist/{chunk-WY6PP6AA.js → chunk-PQH56YF6.js} +37 -5
  170. package/dist/chunk-PRTBPXYA.js +50 -0
  171. package/dist/{chunk-YEDDOYSC.js → chunk-PRW3HXM3.js} +2 -2
  172. package/dist/chunk-PRZ7KMWL.js +42 -0
  173. package/dist/{chunk-6M5NK3K3.js → chunk-PTQ3ZALZ.js} +42 -4
  174. package/dist/{chunk-5GDRCDQB.js → chunk-PUEWDYGP.js} +2 -2
  175. package/dist/chunk-Q26LDIUV.js +236 -0
  176. package/dist/{chunk-CR3MTH32.js → chunk-Q46QDDMF.js} +77 -7
  177. package/dist/chunk-QBNWF5ZU.js +19 -0
  178. package/dist/chunk-QE527XRY.js +342 -0
  179. package/dist/{chunk-CNUKCS2T.js → chunk-QHJJMZLX.js} +1518 -54
  180. package/dist/chunk-QJCJOMW3.js +425 -0
  181. package/dist/{chunk-VYDBJ73M.js → chunk-QKMRFNMA.js} +15 -15
  182. package/dist/chunk-QOOEYDNJ.js +317 -0
  183. package/dist/chunk-QSIDVFB7.js +45 -0
  184. package/dist/{chunk-R77KLRB3.js → chunk-RA33EYS4.js} +19 -18
  185. package/dist/{chunk-DFBCSHVT.js → chunk-RALY3E5U.js} +4 -4
  186. package/dist/{chunk-ZCJDOK2S.js → chunk-S2RY3BOD.js} +35 -21
  187. package/dist/chunk-S5LSDXV4.js +40 -0
  188. package/dist/chunk-SB5GRUFM.js +54 -0
  189. package/dist/chunk-SH27FYPF.js +150 -0
  190. package/dist/chunk-T74WQQGS.js +40 -0
  191. package/dist/{chunk-O3WI5KWW.js → chunk-TGVVUKTG.js} +2 -2
  192. package/dist/chunk-THN26IEF.js +48 -0
  193. package/dist/chunk-TJH2EUAZ.js +47 -0
  194. package/dist/{chunk-ZNCEKR4J.js → chunk-TQ4TG57V.js} +12 -8
  195. package/dist/{chunk-7FBGJFEE.js → chunk-TQTRWENN.js} +12 -12
  196. package/dist/{chunk-TYAID7B7.js → chunk-TSM6T6GQ.js} +23 -23
  197. package/dist/{chunk-DPZHZQKE.js → chunk-TW3KOSQK.js} +43 -10
  198. package/dist/{chunk-MC3H7736.js → chunk-TWQRPM76.js} +37 -4
  199. package/dist/chunk-TYD6I4CZ.js +109 -0
  200. package/dist/chunk-U3NBK4EU.js +420 -0
  201. package/dist/{chunk-6JCPI55B.js → chunk-UDXWIPCB.js} +47 -7
  202. package/dist/{chunk-MMOSZPWJ.js → chunk-UEVZZZU2.js} +4 -4
  203. package/dist/{chunk-JBTK5OP2.js → chunk-UF4UHCIG.js} +2 -2
  204. package/dist/{chunk-6JUBZBMW.js → chunk-UFE36WJH.js} +4 -4
  205. package/dist/{chunk-ONLS5QA5.js → chunk-URIKUID7.js} +12 -12
  206. package/dist/chunk-UYLBRRFK.js +1106 -0
  207. package/dist/chunk-UZACEG2C.js +515 -0
  208. package/dist/{chunk-7SW2QWLU.js → chunk-VAMHOHUU.js} +12 -12
  209. package/dist/{chunk-RZPYKDWS.js → chunk-VUVGX5L2.js} +1 -1
  210. package/dist/{chunk-Q4D5X7QB.js → chunk-VYOCPTUB.js} +44 -24
  211. package/dist/{chunk-CAQZUNOH.js → chunk-WAZEXEJH.js} +567 -3
  212. package/dist/chunk-WCEF7H6L.js +221 -0
  213. package/dist/{chunk-2DIQQJCB.js → chunk-WEOMJHPV.js} +12 -12
  214. package/dist/{chunk-FHH4I4ZV.js → chunk-WFU77T3A.js} +12 -6
  215. package/dist/chunk-WIWTJMMR.js +292 -0
  216. package/dist/{chunk-2BFCZ3RK.js → chunk-WNTIOBOG.js} +18 -18
  217. package/dist/{chunk-7WD63HU2.js → chunk-X2CUXTWU.js} +6 -6
  218. package/dist/chunk-X7EMBM5X.js +34 -0
  219. package/dist/chunk-XEQCQO66.js +33 -0
  220. package/dist/{chunk-F7HYOUFF.js → chunk-XG4WJHLQ.js} +20 -4
  221. package/dist/{chunk-EFHQ3TWP.js → chunk-XOL3GFW2.js} +171 -31
  222. package/dist/{chunk-NE2W3OO7.js → chunk-XQ3YZ4QW.js} +22 -22
  223. package/dist/{chunk-CEXSU4S6.js → chunk-Y6K3CJBU.js} +723 -85
  224. package/dist/{chunk-4RS5KZQD.js → chunk-YAH5IQAL.js} +5 -5
  225. package/dist/{chunk-64EQYWCA.js → chunk-YBOVNMHC.js} +51 -14
  226. package/dist/chunk-YF5DZCPN.js +1212 -0
  227. package/dist/chunk-YIGBJNH4.js +37 -0
  228. package/dist/{chunk-BKVWTZ6A.js → chunk-YJ3TBOTK.js} +25 -25
  229. package/dist/{chunk-Q5AMFEWD.js → chunk-YKTE2RU3.js} +2 -2
  230. package/dist/{chunk-7EKXJZ2Y.js → chunk-YO2PKTXT.js} +296 -37
  231. package/dist/{chunk-6J5OKA4Y.js → chunk-YTTNTQOX.js} +15 -15
  232. package/dist/{chunk-OT2O76OS.js → chunk-YWAKDNMH.js} +183 -33
  233. package/dist/{chunk-R6XF6UR3.js → chunk-YZDZOEV4.js} +4 -4
  234. package/dist/chunk-Z3QFH7YG.js +17 -0
  235. package/dist/{chunk-W5BVQF44.js → chunk-Z6VLC5X4.js} +17 -17
  236. package/dist/{chunk-HLJAXMM4.js → chunk-ZFQMMU5Y.js} +2 -2
  237. package/dist/{chunk-W3BQAZ3Q.js → chunk-ZKYRDV7A.js} +12 -12
  238. package/dist/{chunk-ZMAINWBW.js → chunk-ZLS6PU2O.js} +3 -3
  239. package/dist/{chunk-ZVM6AKKM.js → chunk-ZOSD4FXV.js} +3 -3
  240. package/dist/chunk-ZPDDEVQX.js +47 -0
  241. package/dist/{chunk-TXLH6WHQ.js → chunk-ZQLRCA2V.js} +11 -11
  242. package/dist/{chunk-HITVBIJS.js → chunk-ZX7235KR.js} +128 -54
  243. package/dist/{chunk-GT4I4ZWM.js → chunk-ZZFHLRQW.js} +18 -16
  244. package/dist/files.json +636 -364
  245. package/dist/packages/naysayer/constitution/constitution.yaml +19 -0
  246. package/dist/packages/sdk/npm/index.js +5614 -2343
  247. package/dist/packages/ui/dist/assets/{elk-api-tma0MCuh.js → elk-api-CowTnhEJ.js} +2 -2
  248. package/dist/packages/ui/dist/assets/index-Dw20IRhQ.css +1 -0
  249. package/dist/packages/ui/dist/assets/index-z2wv0d5d.js +47 -0
  250. package/dist/packages/ui/dist/index.html +2 -2
  251. package/package.json +4 -4
  252. package/dist/chunk-2BF2EYJV.js +0 -174
  253. package/dist/chunk-42EVSKME.js +0 -37
  254. package/dist/chunk-4BKGMUIK.js +0 -53
  255. package/dist/chunk-5KLGBXG5.js +0 -48
  256. package/dist/chunk-6BNC6EQ4.js +0 -41
  257. package/dist/chunk-6D2CQ5II.js +0 -41
  258. package/dist/chunk-7AVHLZGD.js +0 -45
  259. package/dist/chunk-D46WUV3T.js +0 -33
  260. package/dist/chunk-E36QOJMD.js +0 -47
  261. package/dist/chunk-FQXYPF7E.js +0 -147
  262. package/dist/chunk-GBRFVT63.js +0 -45
  263. package/dist/chunk-ICY36XCH.js +0 -34
  264. package/dist/chunk-J4K3KVLD.js +0 -131
  265. package/dist/chunk-JZU3H6UQ.js +0 -54
  266. package/dist/chunk-NN2ZQEB4.js +0 -38
  267. package/dist/chunk-PCOCACY3.js +0 -385
  268. package/dist/chunk-RTEYOSJY.js +0 -29
  269. package/dist/chunk-RVH5SUW5.js +0 -69
  270. package/dist/chunk-VN3DP4LQ.js +0 -43
  271. package/dist/chunk-XPTOBS2D.js +0 -48
  272. package/dist/packages/ui/dist/assets/index-B54fm9FQ.js +0 -29
  273. 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-64AADMTW.js";
11
11
  import {
12
12
  dirSource,
13
+ rereadPlan,
13
14
  sameCommit,
14
15
  verifyFacts
15
- } from "./chunk-PAMPPUR6.js";
16
+ } from "./chunk-JYMIB5XY.js";
16
17
  import {
17
18
  scanInputs
18
- } from "./chunk-23W7VWKR.js";
19
+ } from "./chunk-CPK7QSBG.js";
19
20
  import {
20
21
  listIngestors
21
- } from "./chunk-7EKXJZ2Y.js";
22
+ } from "./chunk-YO2PKTXT.js";
22
23
  import {
23
24
  BUILT_IN_EXTRACTORS,
24
25
  MODEL_READERS,
@@ -33,12 +34,17 @@ import {
33
34
  runDialectTests,
34
35
  runFrameworkTests,
35
36
  unmodelledFrameworks
36
- } from "./chunk-MC3H7736.js";
37
+ } from "./chunk-TWQRPM76.js";
37
38
  import {
38
39
  baseClonePath,
39
40
  changesSince,
40
- checkoutState
41
- } from "./chunk-EFHQ3TWP.js";
41
+ checkoutState,
42
+ inPlaceDir
43
+ } from "./chunk-XOL3GFW2.js";
44
+ import {
45
+ buildContextPack,
46
+ toRunSummary
47
+ } from "./chunk-QHJJMZLX.js";
42
48
  import {
43
49
  DEFAULT_IGNORES,
44
50
  configHash,
@@ -50,49 +56,71 @@ import {
50
56
  sandboxedExtractor,
51
57
  scanRepo,
52
58
  withParseCache
53
- } from "./chunk-GQ4N4WKF.js";
54
- import {
55
- buildContextPack,
56
- toRunSummary
57
- } from "./chunk-CNUKCS2T.js";
59
+ } from "./chunk-7AB6OWTV.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
+ conventionsOf,
73
87
  declaredCallsOf,
74
88
  deserializeRecord,
89
+ effectiveComponentTypes,
75
90
  external_exports,
76
91
  factIdPart,
77
92
  factsDirOf,
78
93
  factsRelOf,
79
94
  fieldKey,
80
95
  flowFiles,
96
+ helperOn,
81
97
  hostMatches,
82
98
  hostQuestionText,
83
99
  identifierWords,
100
+ isAiBy,
101
+ isGuessedJoin,
102
+ isPathMatch,
84
103
  isPrototypeType,
85
104
  joinCallers,
105
+ joinConventions,
86
106
  joinFacts,
107
+ joinFlagsOf,
108
+ joinGuessesOf,
87
109
  joinRecords,
110
+ joinSharedMaps,
111
+ joinTerms,
88
112
  lineNamesField,
89
113
  lineNamesRoute,
90
114
  lineNamesWord,
115
+ linkEvidenceOf,
91
116
  listQuestions,
92
117
  loadOrgPack,
93
118
  mergeCatalog,
94
119
  normalizePath,
95
120
  orgPackDirOf,
121
+ outsideSystemOf,
122
+ pairsFromCorrections,
123
+ parseCorrections,
96
124
  parseFacts,
97
125
  parseRecord,
98
126
  projectFacts,
@@ -101,267 +129,32 @@ import {
101
129
  recordPath,
102
130
  require_dist,
103
131
  serializeRecord,
132
+ sharedJoinAbout,
133
+ singularNoun,
104
134
  slugify,
105
135
  specFileHash,
106
136
  specFindingFingerprint,
137
+ stackHelpersOff,
138
+ stripLinkEvidence,
139
+ stripSharedMaps,
140
+ termGrantRecords,
107
141
  throwIfCancelled,
108
142
  toCanonicalYaml,
143
+ topicQuestionText,
109
144
  urlHostPort
110
- } from "./chunk-S2A7YC3Q.js";
145
+ } from "./chunk-PAINMSID.js";
111
146
  import {
112
147
  SCHEMA_VERSION,
113
148
  writeFileAtomic
114
- } from "./chunk-D3PO4DOZ.js";
149
+ } from "./chunk-ODMBPDDV.js";
115
150
  import {
116
151
  __toESM
117
152
  } from "./chunk-TV45EUGM.js";
118
153
 
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;
148
- }
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 };
180
- }
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
- });
196
- }
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();
228
- }
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;
279
- }
280
- function undefinedCodeTerms(entries) {
281
- return entries.filter((e) => e.sources.length === 1 && e.sources[0] === "code" && (e.code?.uses.length ?? 0) > 0);
282
- }
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
- };
359
- }
360
-
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";
154
+ // packages/scanner/dist/scan.js
155
+ import { stat } from "node:fs/promises";
156
+ import { dirname as dirname3, isAbsolute, join as join9, resolve as resolve2 } from "node:path";
157
+ import { fileURLToPath } from "node:url";
365
158
 
366
159
  // packages/glossary/dist/words.js
367
160
  var STOP_TOKENS = /* @__PURE__ */ new Set([
@@ -407,21 +200,7 @@ var QUALIFIERS = /* @__PURE__ */ new Set([
407
200
  "previous"
408
201
  ]);
409
202
  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
- }
203
+ var singular = (w) => singularNoun(w);
425
204
  var isNoise = (w) => STOP_TOKENS.has(w) || /^v\d+$/.test(w) || /^\d+$/.test(w);
426
205
  function businessWords(s) {
427
206
  return splitWords(s).filter((w) => !isNoise(w)).map(singular);
@@ -457,101 +236,125 @@ function definitionKey(d) {
457
236
  return d.toLowerCase().replace(/[^a-z0-9]+/g, " ").trim();
458
237
  }
459
238
 
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);
239
+ // packages/glossary/dist/wiki.js
240
+ var GLOSSARY = /\b(glossary|glossaries|definitions|terminology|terms|vocabulary)\b/i;
241
+ 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;
242
+ var strip = (s) => s.replace(/^\*\*(.+)\*\*$/, "$1").replace(/^__(.+)__$/, "$1").replace(/[`]/g, "").trim();
243
+ function isTermLike(raw) {
244
+ const t = strip(raw);
245
+ if (!t || t.length > 60 || !/^[A-Za-z]/.test(t))
246
+ return false;
247
+ if (/:\/\/|[|?!;]/.test(t) || /[.,]$/.test(t))
248
+ return false;
249
+ if (META.test(t))
250
+ return false;
251
+ const words2 = t.split(/\s+/);
252
+ return words2.length >= 1 && words2.length <= 5;
524
253
  }
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);
254
+ var cleanDefinition = (s) => s.replace(/\s+/g, " ").trim();
255
+ var BOLD = /^(?:\*\*([^*]+?)\*\*|__([^_]+?)__)\s*(?::|\s[—–-]\s)\s*(.+)$/;
256
+ var PLAIN = /^([A-Za-z][^:]{0,59}?)\s*(?::|\s[—–-]\s)\s*(.+)$/;
257
+ function lineDefinition(text2, scoped) {
258
+ const b = BOLD.exec(text2);
259
+ if (b) {
260
+ const term = (b[1] ?? b[2]).trim();
261
+ const definition = cleanDefinition(b[3]);
262
+ if (isTermLike(term) && definition.length >= 3)
263
+ return { term, definition, markup: "bold" };
264
+ return void 0;
536
265
  }
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);
266
+ if (!scoped)
267
+ return void 0;
268
+ const p = PLAIN.exec(text2);
269
+ if (p && isTermLike(p[1])) {
270
+ const definition = cleanDefinition(p[2]);
271
+ if (definition.length >= 3)
272
+ return { term: strip(p[1]), definition, markup: "line" };
273
+ }
274
+ return void 0;
539
275
  }
540
- async function writeDecisions(curatedDir, decisions) {
276
+ var TERM_HEADER = /^(term|terms|name|word|phrase|concept)$/i;
277
+ var DEF_HEADER = /^(definition|definitions|meaning|description|what it means)$/i;
278
+ function scopes(page) {
279
+ const fmType = [page.frontMatter.type, page.frontMatter.kind].filter((v) => typeof v === "string").join(" ");
280
+ const pageWide = GLOSSARY.test(page.title) || GLOSSARY.test(fmType);
541
281
  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);
282
+ let openLevel;
283
+ for (const s of page.sections) {
284
+ if (openLevel !== void 0 && s.level > 0 && s.level <= openLevel)
285
+ openLevel = void 0;
286
+ if (openLevel === void 0 && s.level > 0 && GLOSSARY.test(s.heading))
287
+ openLevel = s.level;
288
+ out.push(pageWide || openLevel !== void 0);
548
289
  }
549
290
  return out;
550
291
  }
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";
292
+ function pageDefinitions(page) {
293
+ const out = [];
294
+ const add = (term, definition, line, markup) => {
295
+ const t = displayTerm(strip(term));
296
+ const key = termKey(t);
297
+ if (!key)
298
+ return;
299
+ out.push({
300
+ term: t,
301
+ key,
302
+ definition,
303
+ path: page.path,
304
+ line,
305
+ markup,
306
+ pageTitle: page.title,
307
+ ...page.owner ? { owner: page.owner } : {}
308
+ });
309
+ };
310
+ const inScope = scopes(page);
311
+ page.sections.forEach((s, si) => {
312
+ const scoped = inScope[si];
313
+ if (scoped && s.level >= 2 && !GLOSSARY.test(s.heading) && isTermLike(s.heading)) {
314
+ const first = s.blocks[0];
315
+ if (first?.type === "paragraph" && !lineDefinition(first.text.split("\n")[0], true))
316
+ add(s.heading, cleanDefinition(first.text), first.line, "heading");
317
+ }
318
+ for (const b of s.blocks) {
319
+ if (b.type === "paragraph") {
320
+ const lines = b.text.split("\n");
321
+ for (let k = 0; k < lines.length; k++) {
322
+ const l = lines[k];
323
+ const next = lines[k + 1];
324
+ if (next !== void 0 && /^:\s+\S/.test(next) && isTermLike(l)) {
325
+ add(l, cleanDefinition(next.replace(/^:\s+/, "")), b.line + k, "definition-list");
326
+ k++;
327
+ continue;
328
+ }
329
+ const d = lineDefinition(l, scoped);
330
+ if (d)
331
+ add(d.term, d.definition, b.line + k, d.markup);
332
+ }
333
+ } else if (b.type === "list") {
334
+ for (const it of b.items) {
335
+ const d = lineDefinition(it.text.split("\n")[0], scoped);
336
+ if (d)
337
+ add(d.term, cleanDefinition([d.definition, ...it.text.split("\n").slice(1)].join(" ")), it.line, d.markup === "line" ? "list" : d.markup);
338
+ }
339
+ } else if (b.type === "table" && scoped) {
340
+ const ti = b.header.findIndex((h) => TERM_HEADER.test(strip(h)));
341
+ const di = b.header.findIndex((h) => DEF_HEADER.test(strip(h)));
342
+ if (ti < 0 || di < 0)
343
+ continue;
344
+ for (const r of b.rows) {
345
+ const term = r.cells[ti] ?? "";
346
+ const def = cleanDefinition(r.cells[di] ?? "");
347
+ if (isTermLike(term) && def.length >= 3)
348
+ add(term, def, r.line, "table");
349
+ }
350
+ }
351
+ }
352
+ });
353
+ return out.sort((a, b) => a.line - b.line);
354
+ }
355
+ function wikiDefinitions(pages) {
356
+ return pages.flatMap(pageDefinitions).sort((a, b) => a.path === b.path ? a.line - b.line : a.path < b.path ? -1 : 1);
357
+ }
555
358
 
556
359
  // packages/glossary/dist/code.js
557
360
  var firstEvidence = (r) => r.evidence[0];
@@ -700,148 +503,28 @@ function fieldNames(records) {
700
503
  return named(records).filter((n) => n.isField).map((n) => ({ record: n.record, name: n.name }));
701
504
  }
702
505
 
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;
506
+ // packages/glossary/dist/build.js
507
+ var isDecision = (r) => r.kind === "CuratedDecision" && r.status === "current";
508
+ var isObj = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
509
+ function typeSort(type) {
510
+ const t = type.toLowerCase();
511
+ if (/^(numeric|decimal|number|money|int|integer|bigint|smallint|tinyint|float|double|real|long|short)\b/.test(t))
512
+ return "number";
513
+ if (/^(date|time|timestamp|datetime|timestamptz|instant|localdate)/.test(t))
514
+ return "time";
515
+ if (/^(bool|boolean|bit)\b/.test(t))
516
+ return "boolean";
517
+ if (/^(uuid|guid)\b/.test(t))
518
+ return "identifier";
519
+ if (/^(varchar|char|character|nvarchar|nchar|text|string|clob|citext)/.test(t))
520
+ return "text";
521
+ return t.replace(/\(.*$/, "");
717
522
  }
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
- // packages/glossary/dist/build.js
824
- var isDecision = (r) => r.kind === "CuratedDecision" && r.status === "current";
825
- var isObj = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
826
- function typeSort(type) {
827
- const t = type.toLowerCase();
828
- if (/^(numeric|decimal|number|money|int|integer|bigint|smallint|tinyint|float|double|real|long|short)\b/.test(t))
829
- return "number";
830
- if (/^(date|time|timestamp|datetime|timestamptz|instant|localdate)/.test(t))
831
- return "time";
832
- if (/^(bool|boolean|bit)\b/.test(t))
833
- return "boolean";
834
- if (/^(uuid|guid)\b/.test(t))
835
- return "identifier";
836
- if (/^(varchar|char|character|nvarchar|nchar|text|string|clob|citext)/.test(t))
837
- return "text";
838
- return t.replace(/\(.*$/, "");
839
- }
840
- function definitionSort(d) {
841
- if (/\b(amount|sum|total|count|number of|quantity)\b/i.test(d))
842
- return "number";
843
- if (/\b(date|timestamp|time at which|day on which)\b/i.test(d))
844
- return "time";
523
+ function definitionSort(d) {
524
+ if (/\b(amount|sum|total|count|number of|quantity)\b/i.test(d))
525
+ return "number";
526
+ if (/\b(date|timestamp|time at which|day on which)\b/i.test(d))
527
+ return "time";
845
528
  return void 0;
846
529
  }
847
530
  var ALIASES = (d) => (Array.isArray(d.value) ? d.value : [d.value]).map(String);
@@ -1021,38 +704,377 @@ function buildGlossary(input) {
1021
704
  for (const col of linkedColumns)
1022
705
  columnLinks.push({ column: col, term: id, how: "decision" });
1023
706
  }
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
- };
707
+ const decided = new Set(columnLinks.filter((l) => l.how === "decision").map((l) => l.column));
708
+ 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));
709
+ const covered = /* @__PURE__ */ new Set([...terms.flatMap((t) => t.fields), ...links.map((l) => l.column)]);
710
+ const names = entries.map((e) => ({ id: e.id, term: e.term, aliases: e.aliases }));
711
+ const hints = fieldNames(records).filter((f) => !covered.has(f.record)).map((f) => namingHint(f.record, f.name, names)).filter((h) => !!h);
712
+ return {
713
+ terms,
714
+ entries,
715
+ columnLinks: links,
716
+ conflicts: conflicts.sort((a, b) => a.term.localeCompare(b.term) || a.kind.localeCompare(b.kind)),
717
+ hints,
718
+ problems
719
+ };
720
+ }
721
+ function applyGlossary(records, build) {
722
+ const link = /* @__PURE__ */ new Map();
723
+ for (const l of build.columnLinks)
724
+ if (!link.has(l.column))
725
+ link.set(l.column, l.term);
726
+ const out = [];
727
+ for (const r of records) {
728
+ if (r.kind === "GlossaryTerm")
729
+ continue;
730
+ if (r.kind === "Column" && !r.glossaryTerm && link.has(r.id))
731
+ out.push({ ...r, glossaryTerm: link.get(r.id) });
732
+ else
733
+ out.push(r);
734
+ }
735
+ return [...out, ...build.terms];
736
+ }
737
+
738
+ // packages/glossary/dist/proposals.js
739
+ var import_yaml = __toESM(require_dist(), 1);
740
+ import { mkdir, readdir, readFile, rm, writeFile } from "node:fs/promises";
741
+ import { join } from "node:path";
742
+ var GLOSSARY_DEFINER = "glossary-definer";
743
+ var GLOSSARY_CHANGE = "glossary";
744
+ var GLOSSARY_OUTPUT = "catalog/proposals/glossary-definitions.yaml";
745
+ var DefinitionProposalFile = external_exports.strictObject({
746
+ schemaVersion: external_exports.literal(1),
747
+ term: RecordId,
748
+ definition: external_exports.string().min(1),
749
+ evidence: external_exports.array(external_exports.string().min(1)).min(1),
750
+ agent: RecordId,
751
+ runId: external_exports.string().optional(),
752
+ at: IsoDateTime
753
+ });
754
+ var LEADING = ["schemaVersion", "term", "definition", "evidence"];
755
+ async function writeDefinitionProposals(dir, proposals) {
756
+ const written = [];
757
+ if (!proposals.length)
758
+ return written;
759
+ await mkdir(join(dir, "glossaryterm"), { recursive: true });
760
+ for (const p of proposals) {
761
+ const file = `glossaryterm/${p.term}.yaml`;
762
+ const body = DefinitionProposalFile.parse({ schemaVersion: 1, ...p });
763
+ await writeFile(join(dir, file), toCanonicalYaml(body, LEADING), "utf8");
764
+ written.push(file);
765
+ }
766
+ return written;
767
+ }
768
+ async function readDefinitionProposals(dir) {
769
+ const proposals = [];
770
+ const problems = [];
771
+ let files;
772
+ try {
773
+ files = (await readdir(join(dir, "glossaryterm"))).filter((f) => /\.ya?ml$/.test(f)).sort();
774
+ } catch {
775
+ return { proposals, problems };
776
+ }
777
+ for (const f of files) {
778
+ const file = `glossaryterm/${f}`;
779
+ let data;
780
+ try {
781
+ data = (0, import_yaml.parse)(await readFile(join(dir, file), "utf8"));
782
+ } catch (e) {
783
+ problems.push({ file, message: `invalid YAML: ${e.message.split("\n")[0]}` });
784
+ continue;
785
+ }
786
+ const p = DefinitionProposalFile.safeParse(data);
787
+ if (!p.success) {
788
+ problems.push({
789
+ file,
790
+ message: p.error.issues.map((i) => `${i.path.join(".")}: ${i.message}`).join("; ")
791
+ });
792
+ continue;
793
+ }
794
+ const { schemaVersion: _v, ...rest } = p.data;
795
+ void _v;
796
+ proposals.push(rest);
797
+ }
798
+ return { proposals, problems };
799
+ }
800
+ function glossaryDefinerAgent(orgPack) {
801
+ const own = orgPack?.agents.find((a) => a.id === GLOSSARY_DEFINER);
802
+ if (own)
803
+ return own;
804
+ return AgentDefinition.parse({
805
+ schemaVersion: 1,
806
+ id: GLOSSARY_DEFINER,
807
+ role: "business-analyst",
808
+ description: "Proposes a one-sentence business definition for each glossary term found only in code, from where the code uses it.",
809
+ inputs: ["glossary", "catalog-slice"],
810
+ // C5 has no output kind for catalog proposals (see ADR 0031); a definition seeds specs.
811
+ outputArtifact: "specs",
812
+ tokenBudget: orgPack?.runtime.budgets.defaultTokens ?? 15e4,
813
+ allowedRuntimes: ["hand-off", "model", "headless-claude-code", "mock", "replay"]
814
+ });
815
+ }
816
+ var INSTRUCTIONS = `# Glossary definer
817
+
818
+ 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:
819
+
820
+ - definition: one sentence a product owner would accept, saying what the word means to the business, only from the evidence;
821
+ - evidence: the record ids from that term's block your definition relies on.
822
+
823
+ Don't invent systems, rules or numbers. A proposal that cites no record id from its term is discarded. Write YAML and nothing else:
824
+
825
+ terms:
826
+ - id: <term id>
827
+ definition: <one sentence>
828
+ evidence: [<record id>, ...]`;
829
+ var Proposal = external_exports.object({
830
+ id: external_exports.string().min(1),
831
+ definition: external_exports.string().trim().min(3).max(400),
832
+ evidence: external_exports.array(external_exports.coerce.string()).default([])
833
+ });
834
+ function block(e) {
835
+ return toCanonicalYaml({
836
+ id: e.id,
837
+ term: e.term,
838
+ usedBy: (e.code?.uses ?? []).map((u) => ({
839
+ id: u.record,
840
+ kind: u.kind,
841
+ name: u.name,
842
+ ...u.evidence.path ? {
843
+ code: `${u.evidence.repo ? `${u.evidence.repo}:` : ""}${u.evidence.path}${u.evidence.line ? `:${u.evidence.line}` : ""}`
844
+ } : {}
845
+ }))
846
+ }, ["id", "term", "usedBy", "id", "kind", "name", "code"]).trimEnd();
847
+ }
848
+ function parseDefinerOutput(output, entries, meta) {
849
+ let data;
850
+ try {
851
+ data = (0, import_yaml.parse)(output.replace(/^```(?:ya?ml)?\s*\n|\n```\s*$/g, ""));
852
+ } catch (e) {
853
+ return {
854
+ proposals: [],
855
+ discarded: [{ reason: `the output is not YAML: ${e.message.split("\n")[0]}` }]
856
+ };
857
+ }
858
+ const list = data?.terms;
859
+ if (!Array.isArray(list))
860
+ return { proposals: [], discarded: [{ reason: "the output has no terms list" }] };
861
+ const byId = new Map(entries.map((e) => [e.id, e]));
862
+ const out = { proposals: [], discarded: [] };
863
+ for (const raw of list) {
864
+ const p = Proposal.safeParse(raw);
865
+ const id = typeof raw?.id === "string" ? raw.id : void 0;
866
+ if (!p.success) {
867
+ out.discarded.push({ ...id ? { term: id } : {}, reason: "incomplete proposal" });
868
+ continue;
869
+ }
870
+ const e = byId.get(p.data.id);
871
+ if (!e) {
872
+ out.discarded.push({
873
+ term: p.data.id,
874
+ reason: `${p.data.id} isn't one of the terms it was given`
875
+ });
876
+ continue;
877
+ }
878
+ if (out.proposals.some((x) => x.term === e.id)) {
879
+ out.discarded.push({ term: e.id, reason: "a second proposal for the same term" });
880
+ continue;
881
+ }
882
+ const known = new Set((e.code?.uses ?? []).map((u) => u.record));
883
+ const evidence = [...new Set(p.data.evidence.filter((x) => known.has(x)))];
884
+ if (!evidence.length) {
885
+ out.discarded.push({ term: e.id, reason: "it cites no record that uses the term" });
886
+ continue;
887
+ }
888
+ out.proposals.push({
889
+ term: e.id,
890
+ definition: p.data.definition.replace(/\s+/g, " "),
891
+ evidence,
892
+ agent: meta.agent,
893
+ at: meta.at,
894
+ ...meta.runId ? { runId: meta.runId } : {}
895
+ });
896
+ }
897
+ return out;
898
+ }
899
+ function undefinedCodeTerms(entries) {
900
+ return entries.filter((e) => e.sources.length === 1 && e.sources[0] === "code" && (e.code?.uses.length ?? 0) > 0);
901
+ }
902
+ async function proposeDefinitions(entries, runtime, o = {}) {
903
+ const todo = [...entries].sort((a, b) => a.id.localeCompare(b.id));
904
+ if (!todo.length)
905
+ return { proposals: [], discarded: [] };
906
+ const agent = glossaryDefinerAgent(o.orgPack);
907
+ const now2 = o.now ?? /* @__PURE__ */ new Date();
908
+ const at3 = now2.toISOString().replace(/\.\d{3}Z$/, "Z");
909
+ const levels = o.orgPack?.pack?.classification ?? [
910
+ "public",
911
+ "internal",
912
+ "confidential",
913
+ "restricted"
914
+ ];
915
+ const sources = [
916
+ {
917
+ id: "0-instructions",
918
+ kind: "instruction",
919
+ origin: "atticus/glossary",
920
+ text: INSTRUCTIONS,
921
+ classification: "public",
922
+ trusted: true
923
+ },
924
+ ...todo.map((e) => ({
925
+ id: e.id,
926
+ kind: "catalog",
927
+ origin: `catalog/generated/glossaryterm/${e.id}.yaml`,
928
+ text: block(e),
929
+ classification: "internal",
930
+ priority: 10
931
+ }))
932
+ ];
933
+ const pack = buildContextPack({
934
+ change: GLOSSARY_CHANGE,
935
+ agent: agent.id,
936
+ budget: o.orgPack?.runtime.budgets.perAgent[agent.id] ?? agent.tokenBudget,
937
+ sources,
938
+ levels,
939
+ maxClassification: levels.includes("internal") ? "internal" : levels.at(-1),
940
+ now: now2
941
+ });
942
+ const sent = todo.filter((e) => pack.manifest.sources.some((s) => s.id === e.id));
943
+ const discarded = todo.filter((e) => !sent.includes(e)).map((e) => ({
944
+ term: e.id,
945
+ reason: "left out of the context pack (budget or classification)"
946
+ }));
947
+ const prepared = await runtime.prepare({
948
+ change: GLOSSARY_CHANGE,
949
+ agent,
950
+ pack,
951
+ outputPath: GLOSSARY_OUTPUT
952
+ });
953
+ const run = await runtime.run(prepared.runId);
954
+ if (run.state === "waiting" || run.state === "prepared")
955
+ return {
956
+ proposals: [],
957
+ discarded,
958
+ waiting: run.runId,
959
+ promptSha256: pack.manifest.promptSha256
960
+ };
961
+ if (run.state !== "validated" || run.output === void 0)
962
+ return {
963
+ proposals: [],
964
+ discarded: [
965
+ ...discarded,
966
+ { reason: `the glossary definer ${run.state}: ${run.errors.join("; ") || "no output"}` }
967
+ ],
968
+ run: toRunSummary(run),
969
+ promptSha256: pack.manifest.promptSha256
970
+ };
971
+ const parsed = parseDefinerOutput(run.output, sent, { agent: agent.id, at: at3, runId: run.runId });
972
+ return {
973
+ proposals: parsed.proposals,
974
+ discarded: [...discarded, ...parsed.discarded],
975
+ run: toRunSummary(run),
976
+ promptSha256: pack.manifest.promptSha256
977
+ };
978
+ }
979
+
980
+ // packages/glossary/dist/decisions.js
981
+ import { createHash } from "node:crypto";
982
+ import { mkdir as mkdir2, writeFile as writeFile2 } from "node:fs/promises";
983
+ import { dirname, join as join2 } from "node:path";
984
+ var GlossaryError = class extends Error {
985
+ code;
986
+ constructor(code, message) {
987
+ super(message);
988
+ this.code = code;
989
+ this.name = "GlossaryError";
990
+ }
991
+ };
992
+ var now = () => (/* @__PURE__ */ new Date()).toISOString().replace(/\.\d{3}Z$/, "Z");
993
+ function decisionId(prefix, key) {
994
+ const id = `${prefix}.${key}`;
995
+ if (id.length <= 120)
996
+ return id;
997
+ return `${id.slice(0, 110)}-${createHash("sha256").update(id).digest("hex").slice(0, 8)}`;
998
+ }
999
+ function decision(id, target, action, value, reason, o) {
1000
+ if (!o.by.trim())
1001
+ throw new GlossaryError("invalid", "Say who decides (--by <handle>).");
1002
+ const p = parseRecord({
1003
+ schemaVersion: 2,
1004
+ kind: "CuratedDecision",
1005
+ id,
1006
+ status: "current",
1007
+ confidence: "asserted",
1008
+ evidence: [{ source: "manual", by: o.by }],
1009
+ target,
1010
+ action,
1011
+ value,
1012
+ reason: o.reason?.trim() || reason,
1013
+ by: o.by,
1014
+ at: o.at ?? now()
1015
+ });
1016
+ if (!p.ok)
1017
+ throw new GlossaryError("invalid", `That decision isn't valid: ${p.issues.map((i) => `${i.path}: ${i.message}`).join("; ")}`);
1018
+ return p.record;
1019
+ }
1020
+ function resolveTermId(termOrId) {
1021
+ const t = termOrId.trim();
1022
+ if (/^term\.[a-z0-9][a-z0-9._-]*$/.test(t))
1023
+ return t;
1024
+ const id = termId(t);
1025
+ if (id === "term.")
1026
+ throw new GlossaryError("invalid", `"${termOrId}" is not a term.`);
1027
+ return id;
1028
+ }
1029
+ function defineDecision(term, definition, o) {
1030
+ const def = definition.trim().replace(/\s+/g, " ");
1031
+ if (def.length < 3)
1032
+ throw new GlossaryError("invalid", "A definition needs a sentence.");
1033
+ const id = resolveTermId(term);
1034
+ const name = o.existing?.term ?? (term.startsWith("term.") ? void 0 : displayTerm(term));
1035
+ 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);
1036
+ }
1037
+ function aliasDecision(term, aliases, o) {
1038
+ const id = resolveTermId(term);
1039
+ const byKey = /* @__PURE__ */ new Map();
1040
+ for (const a of aliases.map((x) => x.trim().replace(/\s+/g, " ")).filter(Boolean))
1041
+ if (!byKey.has(termKey(a)))
1042
+ byKey.set(termKey(a), a);
1043
+ const names = [...byKey.values()].sort();
1044
+ if (!names.length)
1045
+ throw new GlossaryError("invalid", "Give at least one other name.");
1046
+ return decision(decisionId("glossary.alias", id.slice("term.".length)), id, "alias", names, `Other names for ${id}`, o);
1047
+ }
1048
+ function linkDecision(term, recordId, records, o) {
1049
+ const byId = new Map(records.map((r) => [r.id, r]));
1050
+ const [base2, field] = recordId.split("#");
1051
+ const target = byId.get(base2);
1052
+ if (!target)
1053
+ throw new GlossaryError("not-found", `There is no record ${base2} in the catalog.`);
1054
+ if (field !== void 0) {
1055
+ if (target.kind !== "Schema" || !target.fields.some((f) => f.name === field))
1056
+ throw new GlossaryError("not-found", `${base2} has no schema field ${field}.`);
1057
+ } else if (target.kind === "Column") {
1058
+ return decision(decisionId("glossary.link", target.id), target.id, "assert", { glossaryTerm: term.id }, `${target.id} holds the glossary term ${term.term}`, o);
1059
+ }
1060
+ const fields = [.../* @__PURE__ */ new Set([...term.fields, recordId])].sort();
1061
+ 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
1062
  }
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);
1063
+ async function writeDecisions(curatedDir, decisions) {
1043
1064
  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);
1065
+ for (const d of decisions) {
1066
+ const rel = recordPath(d);
1067
+ const full = join2(curatedDir, ...rel.split("/"));
1068
+ await mkdir2(dirname(full), { recursive: true });
1069
+ await writeFile2(full, serializeRecord(d), "utf8");
1070
+ out.push(rel);
1051
1071
  }
1052
- return [...out, ...build.terms];
1072
+ return out;
1053
1073
  }
1054
1074
 
1055
1075
  // packages/glossary/dist/scan.js
1076
+ import { mkdir as mkdir3, readdir as readdir2, readFile as readFile2, rm as rm2, writeFile as writeFile3 } from "node:fs/promises";
1077
+ import { dirname as dirname2, join as join3, relative, sep } from "node:path";
1056
1078
  async function readRecords(dir) {
1057
1079
  const records = [];
1058
1080
  const problems = [];
@@ -1174,16 +1196,16 @@ async function scanGlossary(root, o = {}) {
1174
1196
  }
1175
1197
  }
1176
1198
  const before = new Map(g.base.map((r) => [`${r.kind}/${r.id}`, r]));
1177
- const put = async (rel, text) => {
1199
+ const put = async (rel, text2) => {
1178
1200
  const full = join3(generated, ...rel.split("/"));
1179
1201
  await mkdir3(dirname2(full), { recursive: true });
1180
- await writeFile3(full, text, "utf8");
1202
+ await writeFile3(full, text2, "utf8");
1181
1203
  written.push(rel);
1182
1204
  };
1183
- for (const [rel, text] of want) {
1205
+ for (const [rel, text2] of want) {
1184
1206
  const cur = await readFile2(join3(generated, ...rel.split("/")), "utf8").catch(() => void 0);
1185
- if (cur !== text)
1186
- await put(rel, text);
1207
+ if (cur !== text2)
1208
+ await put(rel, text2);
1187
1209
  }
1188
1210
  const termIds = new Set(g.terms.map((t) => t.id));
1189
1211
  for (const r of g.records) {
@@ -1221,6 +1243,49 @@ async function completeGlossaryDefinitions(root, run, o = {}) {
1221
1243
  return { ...scanned, propose };
1222
1244
  }
1223
1245
 
1246
+ // packages/scanner/dist/federate-loose.js
1247
+ function resolveFederatedLooseEnds(o) {
1248
+ const originOf = (r) => (r.flags ?? []).find((f) => f.startsWith("federated:"))?.slice("federated:".length);
1249
+ const loose = o.federated.filter((r) => r.kind === "Unresolved");
1250
+ if (!loose.length)
1251
+ return { federated: o.federated, resolved: /* @__PURE__ */ new Map() };
1252
+ const fedComponents = o.federated.filter((r) => r.kind === "Component");
1253
+ const result = o.merge([...o.raw, ...fedComponents, ...loose]);
1254
+ const have = new Set(o.local.map((r) => `${r.kind}/${r.id}`));
1255
+ const known = /* @__PURE__ */ new Set([
1256
+ ...o.local.filter((r) => r.kind === "Component" || r.kind === "System").map((r) => r.id),
1257
+ ...fedComponents.map((r) => r.id)
1258
+ ]);
1259
+ const origin = new Map(fedComponents.map((c) => [c.id, originOf(c)]));
1260
+ const stillLoose = new Set(result.records.filter((r) => r.kind === "Unresolved").map((r) => r.id));
1261
+ const added = [];
1262
+ for (const r of result.records) {
1263
+ if (r.kind !== "Interface" || have.has(`Interface/${r.id}`))
1264
+ continue;
1265
+ const i = r;
1266
+ const from = origin.get(i.from) ?? origin.get(i.to);
1267
+ if (!from || !known.has(i.from) || !known.has(i.to))
1268
+ continue;
1269
+ added.push({ ...r, flags: [.../* @__PURE__ */ new Set([...r.flags ?? [], `federated:${from}`])] });
1270
+ }
1271
+ const key = (e) => JSON.stringify(e);
1272
+ const carried = new Set(added.flatMap((i) => i.evidence.map(key)));
1273
+ const resolved = /* @__PURE__ */ new Map();
1274
+ const kept2 = o.federated.filter((r) => {
1275
+ if (r.kind !== "Unresolved" || stillLoose.has(r.id))
1276
+ return true;
1277
+ if (!r.evidence.some((e) => e.source !== "export" && carried.has(key(e))))
1278
+ return true;
1279
+ const id = originOf(r) ?? "";
1280
+ resolved.set(id, (resolved.get(id) ?? 0) + 1);
1281
+ return false;
1282
+ });
1283
+ return {
1284
+ federated: [...kept2, ...added].sort((a, b) => a.kind === b.kind ? a.id.localeCompare(b.id) : a.kind.localeCompare(b.kind)),
1285
+ resolved
1286
+ };
1287
+ }
1288
+
1224
1289
  // packages/scanner/dist/files.js
1225
1290
  import { readdir as readdir3, readFile as readFile3, rm as rm3 } from "node:fs/promises";
1226
1291
  import { join as join4, relative as relative2, resolve, sep as sep2 } from "node:path";
@@ -1274,8 +1339,8 @@ async function readCatalogFiles(dir, cacheRoot) {
1274
1339
  name: "catalog",
1275
1340
  version: CATALOG_CACHE_VERSION,
1276
1341
  cacheRoot,
1277
- parse: (text) => {
1278
- const r = deserializeRecord(text);
1342
+ parse: (text2) => {
1343
+ const r = deserializeRecord(text2);
1279
1344
  return r.ok ? { ok: true, value: r.record } : { ok: false, message: r.issues.map((i) => `${i.path}: ${i.message}`).join("; ") };
1280
1345
  }
1281
1346
  })) {
@@ -1292,14 +1357,14 @@ async function writeCatalog(dir, records) {
1292
1357
  const wanted = new Map(records.map((r) => [recordPath(r), serializeRecord(r)]));
1293
1358
  let written = 0;
1294
1359
  let unchanged = 0;
1295
- for (const [rel, text] of wanted) {
1360
+ for (const [rel, text2] of wanted) {
1296
1361
  const full = join4(dir, ...rel.split("/"));
1297
1362
  const current = await readFile3(full, "utf8").catch(() => void 0);
1298
- if (current === text) {
1363
+ if (current?.replace(/\r\n?/g, "\n") === text2) {
1299
1364
  unchanged++;
1300
1365
  continue;
1301
1366
  }
1302
- await writeFileAtomic(full, text);
1367
+ await writeFileAtomic(full, text2);
1303
1368
  written++;
1304
1369
  }
1305
1370
  let removed = 0;
@@ -1380,16 +1445,19 @@ function prototypeScanNotes(op, only) {
1380
1445
  function isPrototypeRepo(op, repo) {
1381
1446
  return estateRepos(op).some((r) => r.id === repo && isPrototypeType(r.type));
1382
1447
  }
1383
- function estateMergeInput(op, repos, records, modulesOf = (id) => repos.find((r) => r.id === id)?.modules ?? []) {
1448
+ function estateMergeInput(op, repos, records, modulesOf = (id) => repos.find((r) => r.id === id)?.modules ?? [], read) {
1384
1449
  return {
1385
1450
  records,
1451
+ // ADR 0180: the organisation's component types, so a cited type is never demoted in a merge.
1452
+ componentTypes: new Set(effectiveComponentTypes(op.componentTypes).map((t) => t.id)),
1386
1453
  repos: repos.flatMap((r) => [
1387
1454
  {
1388
1455
  id: r.id,
1389
1456
  type: r.type,
1390
1457
  ...r.component ? { component: r.component } : {},
1391
1458
  ...r.system ? { system: r.system } : {},
1392
- hosts: r.hosts
1459
+ hosts: r.hosts,
1460
+ ...read?.has(r.id) ? { read: true } : {}
1393
1461
  },
1394
1462
  // A module's hosts join calls to its own component (ADR 0057).
1395
1463
  ...modulesOf(r.id).filter((m) => m.hosts.length > 0).map((m) => ({
@@ -1405,15 +1473,346 @@ function estateMergeInput(op, repos, records, modulesOf = (id) => repos.find((r)
1405
1473
  profilePath: op.profiles[0] ? `org-pack/profiles/${op.profiles[0].platform}/profile.yaml` : void 0
1406
1474
  };
1407
1475
  }
1408
- function mergeEstate(op, repos, records, modulesOf) {
1409
- return mergeCatalog(estateMergeInput(op, repos, records, modulesOf));
1476
+ function mergeEstate(op, repos, records, modulesOf, read) {
1477
+ return mergeCatalog(estateMergeInput(op, repos, records, modulesOf, read));
1410
1478
  }
1411
1479
  function curatedDirOf(root, op) {
1412
1480
  return join5(root, ...op.sources.layout.catalog.split("/"), "curated");
1413
1481
  }
1414
1482
 
1483
+ // packages/scanner/dist/facts-scan.js
1484
+ import { readFile as readFile4 } from "node:fs/promises";
1485
+ import { join as join6 } from "node:path";
1486
+
1487
+ // packages/scanner/dist/facts-catalog.js
1488
+ import { createHash as createHash2 } from "node:crypto";
1489
+
1490
+ // packages/scanner/dist/target-suggest.js
1491
+ var TARGETS_MAX_SUGGESTED = 5;
1492
+ var TARGETS_ANOTHER = TARGETS_ANOTHER_LABEL;
1493
+ var TARGETS_NO_EVIDENCE = "no evidence; listed so you can pick it";
1494
+ 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(" "));
1495
+ function nameWords(s) {
1496
+ if (!s)
1497
+ return [];
1498
+ return [
1499
+ ...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))
1500
+ ];
1501
+ }
1502
+ var segs = (p) => p.split(/[?#]/)[0].split("/").filter(Boolean).map((x) => /^[{:<$]/.test(x) ? "{}" : x);
1503
+ var sameSeg = (a, b) => a === "{}" || b === "{}" || a === b;
1504
+ var under = (path, prefix) => {
1505
+ const a = segs(prefix);
1506
+ const b = segs(path);
1507
+ return a.length > 0 && a.length <= b.length && a.every((x, i) => x === b[i]);
1508
+ };
1509
+ function serves(entry, call, bases) {
1510
+ const e = segs(entry);
1511
+ const tries = [
1512
+ segs(call),
1513
+ ...bases.filter((b) => under(call, b)).map((b) => segs(call).slice(segs(b).length))
1514
+ ];
1515
+ return tries.some((c) => c.length === e.length && c.every((x, i) => sameSeg(x, e[i])));
1516
+ }
1517
+ function clientOf(f) {
1518
+ const q = f.cite.quote ?? "";
1519
+ const named2 = /\b([A-Z][A-Za-z0-9]*(?:Client|Service|Api|Feign|Gateway|Proxy))\b/.exec(q)?.[1];
1520
+ if (named2)
1521
+ return named2;
1522
+ const file = f.cite.file.split("/").pop().replace(/\.[^.]+$/, "");
1523
+ return /(Client|Service|Api|Feign|Gateway|Proxy)$/.test(file) ? file : void 0;
1524
+ }
1525
+ function hostOf(f, facts) {
1526
+ const fromUrl = (u) => /^[a-z][a-z0-9+.-]*:\/\/([^/:?#]+)/i.exec(u)?.[1];
1527
+ if (f.host)
1528
+ return fromUrl(f.host) ?? f.host.split(/[/:]/)[0];
1529
+ if (f.base) {
1530
+ const cfg = facts.find((x) => x.kind === "config" && x.key === f.base && !!x.value);
1531
+ if (!cfg?.value)
1532
+ return void 0;
1533
+ return fromUrl(cfg.value) ?? (/^[a-z0-9-]+(\.[a-z0-9-]+)+(:\d+)?$/i.test(cfg.value) ? cfg.value.split(":")[0] : void 0);
1534
+ }
1535
+ return void 0;
1536
+ }
1537
+ var answerLabel = (q) => q.answer?.option !== void 0 ? q.options[q.answer.option - 1]?.label : q.answer?.text;
1538
+ var NOT_A_TARGET = /* @__PURE__ */ new Set(["None of these", TARGETS_SPLIT_LABEL, TARGETS_ANOTHER_LABEL]);
1539
+ function candidates(callerRepo, ctx) {
1540
+ const manifest = estateRepos(ctx.op).filter((r) => r.id !== callerRepo || !!ctx.only);
1541
+ const systems = ctx.op.profiles.flatMap((p) => p.profile?.systems ?? []);
1542
+ const reposOfSystem = /* @__PURE__ */ new Map();
1543
+ for (const r of estateRepos(ctx.op))
1544
+ if (r.system)
1545
+ reposOfSystem.set(r.system, (reposOfSystem.get(r.system) ?? 0) + 1);
1546
+ const read = new Set((ctx.estate ?? []).filter((x) => x.facts.length > 0).map((x) => x.repo));
1547
+ const repoOf = /* @__PURE__ */ new Map();
1548
+ for (const r of ctx.records)
1549
+ if (r.kind === "Component" && r.repo)
1550
+ repoOf.set(r.id, r.repo);
1551
+ if (!ctx.estate)
1552
+ for (const repo of repoOf.values())
1553
+ read.add(repo);
1554
+ const endpoints = ctx.records.filter((r) => r.kind === "Endpoint");
1555
+ const deploys = (ctx.estate ?? []).flatMap((x) => x.facts.filter((f) => f.kind === "deploy" && !!f.basePath));
1556
+ const out = [];
1557
+ for (const m of manifest) {
1558
+ const own2 = new Set([m.id, m.component, ...m.modules.map((x) => x.component)].filter((x) => !!x).map(factIdPart));
1559
+ const sys = m.system && reposOfSystem.get(m.system) === 1 ? systems.find((s) => s.id === m.system) : void 0;
1560
+ const names = /* @__PURE__ */ new Map();
1561
+ for (const n of [m.id, m.component, ...m.modules.map((x) => x.component)])
1562
+ for (const w of nameWords(n))
1563
+ if (!names.has(w))
1564
+ names.set(w, n);
1565
+ for (const a of sys ? [sys.id, ...sys.aliases] : [])
1566
+ for (const w of nameWords(a))
1567
+ if (!names.has(w))
1568
+ names.set(w, a);
1569
+ const bases = [];
1570
+ if (m.basePath)
1571
+ bases.push({ path: m.basePath, from: "the manifest's basePath" });
1572
+ for (const x of m.modules)
1573
+ if (x.basePath)
1574
+ bases.push({ path: x.basePath, from: `the basePath of its module ${x.component}` });
1575
+ for (const d of deploys) {
1576
+ const reaches = d.exposes ? factIdPart(d.exposes) : d.repo === m.id ? m.id : void 0;
1577
+ if (!reaches || !(own2.has(reaches) || d.repo === m.id && !d.exposes))
1578
+ continue;
1579
+ const what2 = d.platform === "apigee" ? "its proxy's BasePath" : `the ${d.resourceType ?? "route"} ${d.resourceName ?? d.id}`;
1580
+ bases.push({ path: d.basePath, from: `${what2} (${d.repo} ${d.cite.file}:${d.cite.line})` });
1581
+ }
1582
+ const comps = [
1583
+ ...new Set(endpoints.filter((e) => repoOf.get(e.component) === m.id).map((e) => e.component))
1584
+ ].sort();
1585
+ const isRead = read.has(m.id);
1586
+ const forLabel = comps.length ? comps : [m.id];
1587
+ for (const label2 of forLabel) {
1588
+ const mine = endpoints.filter((e) => e.component === label2);
1589
+ 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) => ({
1590
+ ...f.method ? { method: f.method.toUpperCase() } : {},
1591
+ path: f.path,
1592
+ at: `${f.cite.file}:${f.cite.line}`
1593
+ }));
1594
+ for (const e of mine)
1595
+ if (!entries.some((x) => x.path === e.pathOrTopic))
1596
+ entries.push({
1597
+ ...e.method ? { method: e.method.toUpperCase() } : {},
1598
+ path: e.pathOrTopic,
1599
+ at: e.id
1600
+ });
1601
+ out.push({
1602
+ s: { label: label2, kind: "repo", repo: m.id, read: isRead, score: 0, why: [] },
1603
+ names,
1604
+ bases,
1605
+ entries,
1606
+ ...m.description?.text ? { description: m.description.text } : {},
1607
+ hosts: m.hosts.map((h) => h.toLowerCase())
1608
+ });
1609
+ }
1610
+ }
1611
+ for (const c of ctx.only ?? [])
1612
+ if (!out.some((x) => x.s.label === c))
1613
+ out.push({
1614
+ s: {
1615
+ label: c,
1616
+ kind: "repo",
1617
+ repo: repoOf.get(c) ?? callerRepo,
1618
+ read: true,
1619
+ score: 0,
1620
+ why: []
1621
+ },
1622
+ names: /* @__PURE__ */ new Map(),
1623
+ bases: [],
1624
+ entries: [],
1625
+ hosts: []
1626
+ });
1627
+ const own = new Set(estateRepos(ctx.op).flatMap((r) => r.system ? [r.system] : []));
1628
+ const seen = /* @__PURE__ */ new Set();
1629
+ for (const s of systems) {
1630
+ if (!(s.boundary === "vendor" || !own.has(s.id)) || seen.has(s.id))
1631
+ continue;
1632
+ seen.add(s.id);
1633
+ const names = /* @__PURE__ */ new Map();
1634
+ for (const n of [s.id, s.name, ...s.aliases])
1635
+ for (const w of nameWords(n))
1636
+ if (!names.has(w))
1637
+ names.set(w, n);
1638
+ out.push({
1639
+ s: {
1640
+ label: s.id,
1641
+ kind: "outside",
1642
+ read: false,
1643
+ name: s.name,
1644
+ boundary: s.boundary,
1645
+ score: 0,
1646
+ why: []
1647
+ },
1648
+ names,
1649
+ bases: [],
1650
+ entries: [],
1651
+ hosts: s.aliases.map((a) => a.toLowerCase())
1652
+ });
1653
+ }
1654
+ return out;
1655
+ }
1656
+ function targetSuggestions(f, ctx) {
1657
+ const all = candidates(f.repo, ctx);
1658
+ const df = /* @__PURE__ */ new Map();
1659
+ for (const c of all)
1660
+ for (const w of c.names.keys())
1661
+ df.set(w, (df.get(w) ?? 0) + 1);
1662
+ const telling = (w) => (df.get(w) ?? 0) <= Math.max(2, Math.floor(all.length / 3));
1663
+ const host = hostOf(f, ctx.facts)?.toLowerCase();
1664
+ const client = clientOf(f);
1665
+ const said = [
1666
+ { from: f.base ? `the config key ${f.base}` : "", words: nameWords(f.base), weight: 4 },
1667
+ { from: client ? `the client ${client}` : "", words: nameWords(client), weight: 4 },
1668
+ {
1669
+ from: host ? `the host label ${host.split(".")[0]} (${host})` : "",
1670
+ words: host ? nameWords(host.split(".")[0]) : [],
1671
+ weight: 4
1672
+ },
1673
+ {
1674
+ from: `the path ${f.path}`,
1675
+ words: nameWords(segs(f.path).filter((x) => x !== "{}").join(" ")),
1676
+ weight: 2
1677
+ }
1678
+ ].filter((x) => x.from && x.words.length);
1679
+ const method = f.method?.toUpperCase();
1680
+ 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}`));
1681
+ const only = ctx.only ? new Set(ctx.only) : void 0;
1682
+ const out = [];
1683
+ for (const c of all) {
1684
+ if (only && c.s.kind === "repo" && !only.has(c.s.label))
1685
+ continue;
1686
+ const s = { ...c.s, why: [] };
1687
+ const add = (n, why) => {
1688
+ s.score += n;
1689
+ s.why.push(why);
1690
+ };
1691
+ if (only?.has(c.s.label))
1692
+ add(6, "the join found an entry of it that serves the call");
1693
+ 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)));
1694
+ if (hit && !only) {
1695
+ const what2 = [hit.method, hit.path].filter(Boolean).join(" ");
1696
+ add(6, `serves ${what2} (${hit.at})`);
1697
+ s.serves = [what2];
1698
+ }
1699
+ const base2 = c.bases.find((b) => under(f.path, b.path));
1700
+ if (base2)
1701
+ add(5, `its basePath ${base2.path} matches: ${base2.from}`);
1702
+ if (host && c.hosts.includes(host))
1703
+ add(8, `it lists ${host} under hosts`);
1704
+ for (const x of said) {
1705
+ const w = x.words.find((y) => c.names.has(y) && telling(y));
1706
+ if (w)
1707
+ add(x.weight, `named like ${x.from}`);
1708
+ }
1709
+ if (c.description) {
1710
+ const d = new Set(nameWords(c.description));
1711
+ const w = said.flatMap((x) => x.words).find((y) => d.has(y) && telling(y));
1712
+ if (w)
1713
+ add(2, `its description mentions ${w}`);
1714
+ }
1715
+ const named2 = earlier.filter((q) => {
1716
+ const l = answerLabel(q);
1717
+ if (!l || NOT_A_TARGET.has(l))
1718
+ return false;
1719
+ const t = slugify(l);
1720
+ return t === slugify(c.s.label) || !!c.s.repo && t === slugify(c.s.repo);
1721
+ }).length;
1722
+ if (named2)
1723
+ add(3, `picked for ${named2} other ${named2 === 1 ? "call" : "calls"} of ${f.repo}`);
1724
+ if (!s.score)
1725
+ s.why.push(TARGETS_NO_EVIDENCE);
1726
+ s.why = [...new Set(s.why)];
1727
+ out.push(s);
1728
+ }
1729
+ 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) || a.label.localeCompare(b.label));
1730
+ }
1731
+ function offeredSuggestions(ranked, o) {
1732
+ const max = o.max ?? TARGETS_MAX_SUGGESTED;
1733
+ const strong = ranked.filter((s) => s.score >= 2);
1734
+ const fill = ranked.filter((s) => s.score < 2 && (s.kind === "outside" || s.read && o.serving.has(s.label)));
1735
+ return [...strong, ...fill].slice(0, max);
1736
+ }
1737
+ function targetPicker(op, read = /* @__PURE__ */ new Set()) {
1738
+ const repos = estateRepos(op).map((r) => ({
1739
+ id: r.id,
1740
+ kind: "repo",
1741
+ name: r.component && r.component !== r.id ? `${r.id} (${r.component})` : r.id,
1742
+ read: read.has(r.id)
1743
+ })).sort((a, b) => a.id.localeCompare(b.id));
1744
+ const seen = /* @__PURE__ */ new Set();
1745
+ 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) => a.id.localeCompare(b.id));
1746
+ const owned = new Set(op.profiles.flatMap((p) => (p.profile?.systems ?? []).flatMap((s) => s.owner ? [s.owner] : [])));
1747
+ const teams = (op.roles?.teams ?? []).map((t) => {
1748
+ const name = t.name ?? t.id;
1749
+ const has = owned.has(t.id);
1750
+ return {
1751
+ id: t.id,
1752
+ kind: "team",
1753
+ name,
1754
+ ...has ? {} : {
1755
+ adds: {
1756
+ id: slugify(`${t.id}-system`),
1757
+ name: `${name}'s system`,
1758
+ boundary: "internal",
1759
+ owner: t.id
1760
+ }
1761
+ }
1762
+ };
1763
+ }).sort((a, b) => a.id.localeCompare(b.id));
1764
+ return [...repos, ...systems, ...teams];
1765
+ }
1766
+ function pickerLines(picks) {
1767
+ const of = (k) => picks.filter((p) => p.kind === k);
1768
+ return [
1769
+ `Repos (${of("repo").length}): ${of("repo").map((p) => `${p.id}${p.read ? "" : " (not read yet)"}`).join(", ") || "none"}`,
1770
+ `Systems (${of("system").length}): ${of("system").map((p) => `${p.id} (${p.boundary})`).join(", ") || "none"}`,
1771
+ `Teams (${of("team").length}): ${of("team").map((p) => `${p.id}${p.adds ? " (no system yet)" : ""}`).join(", ") || "none"}`
1772
+ ];
1773
+ }
1774
+ function topicQuestionDrafts(repo, records) {
1775
+ const out = /* @__PURE__ */ new Map();
1776
+ for (const r of records) {
1777
+ if (r.kind !== "Unresolved")
1778
+ continue;
1779
+ const x = r;
1780
+ if (x.unresolvedKind !== "event-publish" && x.unresolvedKind !== "event-subscribe")
1781
+ continue;
1782
+ const topic = String(x.raw);
1783
+ if (!topic || topic.includes("${") || /\s/.test(topic))
1784
+ continue;
1785
+ const ev = x.evidence.filter((e) => e.repo === repo && e.path && e.line && !/[\s:]/.test(e.path)).map((e) => `${e.path}:${e.line}`).sort();
1786
+ if (!ev.length)
1787
+ continue;
1788
+ const side = x.unresolvedKind === "event-publish" ? "publish" : "listen";
1789
+ const question = topicQuestionText(topic, x.owner ?? repo, side);
1790
+ const draft = {
1791
+ question,
1792
+ options: [
1793
+ {
1794
+ label: TOPICS_OUTSIDE_LABEL,
1795
+ means: `${topic} belongs to another team's system or a vendor's: name it, or the team, from the list`,
1796
+ 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`
1797
+ },
1798
+ {
1799
+ label: "None of these",
1800
+ means: `Nothing outside the estate owns ${topic}, or a repo not read yet serves it`,
1801
+ changes: `It stays a loose end, and this isn't asked again`
1802
+ }
1803
+ ],
1804
+ evidence: [...new Set(ev)],
1805
+ scope: "repo"
1806
+ };
1807
+ const id = questionId({ step: TOPICS_STEP_ID, repo, question, evidence: draft.evidence });
1808
+ if (!out.has(id))
1809
+ out.set(id, { topic, id, draft });
1810
+ }
1811
+ return [...out.values()].sort((a, b) => a.topic.localeCompare(b.topic));
1812
+ }
1813
+
1415
1814
  // packages/scanner/dist/facts-catalog.js
1416
- var TARGETS_STEP = "facts-targets";
1815
+ var TARGETS_STEP = TARGETS_STEP_ID;
1417
1816
  var TARGETS_NONE = "None of these";
1418
1817
  var byKindId = (a, b) => a.kind === b.kind ? a.id.localeCompare(b.id) : a.kind.localeCompare(b.kind);
1419
1818
  var URL_RE = /^[a-z][a-z0-9+.-]*:\/\//i;
@@ -1433,6 +1832,42 @@ var EVENT_CALLS = /* @__PURE__ */ new Set(["event-topic", "mq-queue"]);
1433
1832
  function needsTarget(f, facts) {
1434
1833
  return f.kind === "call" && !f.to && !EVENT_CALLS.has(f.callType) && !(HTTP_CALLS.has(f.callType) && urlOf(f, facts));
1435
1834
  }
1835
+ var UNRESOLVED_HOST = /@[A-Za-z_][A-Za-z0-9_.-]*@|\$\{|\{[^}]*\}/;
1836
+ function ruleMayName(f, facts) {
1837
+ if (f.kind !== "call" || EVENT_CALLS.has(f.callType))
1838
+ return false;
1839
+ if (!f.to && !(HTTP_CALLS.has(f.callType) && urlOf(f, facts)))
1840
+ return true;
1841
+ if (f.to)
1842
+ return f.by !== "person" && UNRESOLVED_HOST.test(f.to);
1843
+ if (!HTTP_CALLS.has(f.callType))
1844
+ return false;
1845
+ const url = urlOf(f, facts);
1846
+ const host = url ? /^[a-z][a-z0-9+.-]*:\/\/([^/?#]*)/i.exec(url)?.[1] ?? "" : "";
1847
+ if (!host || UNRESOLVED_HOST.test(host) || UNRESOLVED_HOST.test(f.host ?? ""))
1848
+ return true;
1849
+ if (f.host || !f.base)
1850
+ return false;
1851
+ 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));
1852
+ return hosts.size > 1;
1853
+ }
1854
+ var MAX_ID = 120;
1855
+ function remapIds(remap) {
1856
+ return (id) => {
1857
+ const exact = remap.get(id);
1858
+ if (exact)
1859
+ return exact;
1860
+ for (const [from, to] of remap)
1861
+ if (id.startsWith(`${from}.`)) {
1862
+ const out = to + id.slice(from.length);
1863
+ if (out.length <= MAX_ID)
1864
+ return out;
1865
+ const hash = createHash2("sha256").update(out, "utf8").digest("hex").slice(0, 16);
1866
+ return `${out.slice(0, MAX_ID - hash.length - 1).replace(/[-._]+$/, "")}.${hash}`;
1867
+ }
1868
+ return id;
1869
+ };
1870
+ }
1436
1871
  function targetQuestionFor(f) {
1437
1872
  if (f.kind !== "call")
1438
1873
  return void 0;
@@ -1449,62 +1884,301 @@ var SERVES = {
1449
1884
  webhook: ["webhook"],
1450
1885
  sftp: ["file-drop"]
1451
1886
  };
1452
- function targetQuestionDrafts(facts, records, op, asks) {
1887
+ function targetAsks(facts, records, asks) {
1453
1888
  const repoOf = /* @__PURE__ */ new Map();
1454
1889
  for (const r of records)
1455
1890
  if (r.kind === "Component" && r.repo)
1456
1891
  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));
1892
+ const list = [];
1893
+ for (const f of [...facts].sort((a, b) => a.id.localeCompare(b.id))) {
1894
+ const ask = asks?.find((a) => a.reason === "ambiguous" && a.call.repo === f.repo && a.call.fact === f.id);
1895
+ if (f.kind !== "call" || !needsTarget(f, facts) && !ask)
1896
+ continue;
1897
+ const own = slugify(f.component ?? f.repo).replace(/\./g, "-");
1898
+ const types = new Set(SERVES[f.callType] ?? []);
1899
+ const servers = ask ? [...new Set(ask.candidates.map((c) => c.component))].sort() : [
1900
+ ...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))
1901
+ ].sort();
1902
+ list.push({ f, q: targetQuestionFor(f), servers });
1903
+ }
1904
+ return { list, repoOf };
1905
+ }
1906
+ var outsideAs = (s) => s.boundary === "vendor" ? "an outside system" : "another team's system";
1907
+ var callWhat = (f) => [f.method?.toUpperCase(), f.path].filter(Boolean).join(" ");
1908
+ function suggestionWhy(s, prefix) {
1909
+ const served = s.serves ?? [];
1910
+ let why = s.why;
1911
+ if (prefix !== void 0 && served.length > 1) {
1912
+ const under2 = prefix === "/" ? "" : ` under ${prefix}`;
1913
+ why = [
1914
+ `serves ${served[0]} and ${served.length - 1} more${under2}`,
1915
+ ...why.filter((w) => !w.startsWith("serves "))
1916
+ ];
1917
+ }
1918
+ return (why.length ? why : [TARGETS_NO_EVIDENCE]).slice(0, 3).join("; ");
1919
+ }
1920
+ function suggestionOption(s, callerRepo, n, prefix) {
1921
+ const why = suggestionWhy(s, prefix);
1922
+ if (s.kind === "outside")
1923
+ return {
1924
+ label: s.label,
1925
+ means: `${s.name}, ${outsideAs({ boundary: s.boundary ?? "vendor" })}: ${why}`,
1926
+ 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`
1927
+ };
1928
+ const who = s.label === s.repo ? `${s.repo}${s.read ? "" : " (not read yet)"}` : `${s.label}, built in ${s.repo}${s.read ? "" : " (not read yet)"}`;
1929
+ const them = n ? `The ${n} calls` : "The call";
1930
+ return {
1931
+ label: s.label,
1932
+ means: `${who}: ${why}`,
1933
+ 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`
1934
+ };
1935
+ }
1936
+ var anotherOption = (op) => {
1937
+ const repos = estateRepos(op).length;
1938
+ const systems = new Set(op.profiles.flatMap((p) => (p.profile?.systems ?? []).map((s) => s.id))).size;
1939
+ const teams = op.roles?.teams?.length ?? 0;
1940
+ return {
1941
+ label: TARGETS_ANOTHER,
1942
+ 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`,
1943
+ 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"
1944
+ };
1945
+ };
1946
+ function offered(members, facts, records, op, asks, ctx) {
1947
+ const merged = /* @__PURE__ */ new Map();
1948
+ for (const m of members) {
1949
+ const ambiguous = asks?.some((a) => a.reason === "ambiguous" && a.call.repo === m.f.repo && a.call.fact === m.f.id);
1950
+ for (const s of targetSuggestions(m.f, {
1951
+ op,
1952
+ records,
1953
+ facts,
1954
+ ...ctx.estate ? { estate: ctx.estate } : {},
1955
+ ...ctx.questions ? { questions: ctx.questions } : {},
1956
+ ...ambiguous ? { only: m.servers } : {}
1957
+ })) {
1958
+ const x = merged.get(s.label);
1959
+ if (!x)
1960
+ merged.set(s.label, { ...s, why: [...s.why], serves: [...s.serves ?? []] });
1961
+ else {
1962
+ x.score += s.score;
1963
+ for (const w of s.why)
1964
+ if (!x.why.includes(w))
1965
+ x.why.push(w);
1966
+ for (const w of s.serves ?? [])
1967
+ if (!x.serves.includes(w))
1968
+ x.serves.push(w);
1969
+ if (x.why.length > 1)
1970
+ x.why = x.why.filter((w) => w !== TARGETS_NO_EVIDENCE);
1971
+ }
1972
+ }
1973
+ }
1974
+ 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) || a.label.localeCompare(b.label));
1975
+ const serving = new Set(members.flatMap((m) => m.servers));
1976
+ const types = new Set(members.flatMap((m) => SERVES[m.f.callType] ?? []));
1977
+ return offeredSuggestions(ranked, { types, serving });
1978
+ }
1979
+ function targetQuestionDrafts(facts, records, op, asks, ctx = {}) {
1980
+ const { list } = targetAsks(facts, records, asks);
1981
+ return list.map((m) => {
1982
+ const { f, q } = m;
1983
+ const draft = {
1984
+ question: q.question,
1985
+ options: [
1986
+ ...offered([m], facts, records, op, asks, ctx).map((s) => suggestionOption(s, f.repo)),
1987
+ anotherOption(op),
1988
+ {
1989
+ label: TARGETS_NONE,
1990
+ means: "Nothing in the estate or the org pack's systems answers it",
1991
+ changes: "The call stays a loose end, and this isn't asked again"
1992
+ }
1993
+ ],
1994
+ evidence: q.evidence,
1995
+ scope: "repo"
1996
+ };
1997
+ return {
1998
+ factId: f.id,
1999
+ id: questionId({
2000
+ step: TARGETS_STEP,
2001
+ repo: f.repo,
2002
+ question: q.question,
2003
+ evidence: q.evidence
2004
+ }),
2005
+ repo: f.repo,
2006
+ draft
2007
+ };
2008
+ });
2009
+ }
2010
+ var TARGETS_SPLIT = TARGETS_SPLIT_LABEL;
2011
+ var PREFIX_ROOT = /^(api|rest|services?|v\d+(\.\d+)*)$/i;
2012
+ function targetPathPrefix(path) {
2013
+ const lead = path.startsWith("/") ? "/" : "";
2014
+ const out = [];
2015
+ for (const s of path.split(/[?#]/)[0].split("/").filter(Boolean)) {
2016
+ if (/^[{:<]/.test(s))
2017
+ break;
2018
+ out.push(s);
2019
+ if (!PREFIX_ROOT.test(s))
2020
+ break;
2021
+ }
2022
+ return out.length ? `${lead}${out.join("/")}` : "/";
2023
+ }
2024
+ function groupVia(f) {
2025
+ if (f.to)
2026
+ return ` naming ${f.to}`;
2027
+ if (f.host)
2028
+ return ` on ${f.host}`;
2029
+ if (f.base)
2030
+ return ` through ${f.base}`;
2031
+ return "";
2032
+ }
2033
+ function targetGroupQuestion(repo, f, facts) {
2034
+ const proxy = facts ? proxyOf(f, facts) : void 0;
2035
+ const through = f.base ?? f.host ?? f.to;
2036
+ if (proxy && through) {
2037
+ const p = targetPathPrefix(f.path);
2038
+ return `Which repo serves the routes under ${p === "/" ? "" : p}/** that ${proxy} forwards to ${through}?`;
2039
+ }
2040
+ return `Which repo or system answers the calls ${repo} makes under ${targetPathPrefix(f.path)}${groupVia(f)}?`;
2041
+ }
2042
+ function proxyOf(f, facts) {
2043
+ if (!f.from)
2044
+ return void 0;
2045
+ const from = facts.find((x) => x.id === f.from);
2046
+ if (!from || from.kind !== "entry")
2047
+ return void 0;
2048
+ const proxy = facts.find((x) => x.kind === "deploy" && x.repo === f.repo && x.platform === "apigee" && !!x.basePath);
2049
+ return proxy ? proxy.resourceName ?? f.repo : void 0;
2050
+ }
2051
+ function targetGroupWordings(repo, f, facts) {
2052
+ return [.../* @__PURE__ */ new Set([targetGroupQuestion(repo, f, facts), targetGroupQuestion(repo, f)])];
2053
+ }
2054
+ var repo0 = (members) => members[0].f.repo;
2055
+ var byLine = (a, b) => a.localeCompare(b, "en", { numeric: true });
2056
+ var answerLabel2 = (q) => q.answer?.option !== void 0 ? q.options[q.answer.option - 1]?.label : q.answer?.text;
2057
+ function splitTargetGroups(questions) {
2058
+ return new Set(questions.filter((q) => q.askedBy?.step === TARGETS_STEP && q.state === "answered" && answerLabel2(q) === TARGETS_SPLIT).map((q) => q.question.trim()));
2059
+ }
2060
+ function groupedTargetDrafts(facts, records, op, asks, o) {
2061
+ const { list } = targetAsks(facts, records, asks);
2062
+ const ctx = {
2063
+ ...o.estate ? { estate: o.estate } : {},
2064
+ ...o.questions ? { questions: o.questions } : {}
2065
+ };
2066
+ const own = new Map(targetQuestionDrafts(facts, records, op, asks, ctx).map((d) => [d.factId, d]));
2067
+ const groups = /* @__PURE__ */ new Map();
2068
+ for (const a of list) {
2069
+ if (!o.ask.has(a.f.id))
2070
+ continue;
2071
+ const k = targetGroupQuestion(a.f.repo, a.f, facts);
2072
+ groups.set(k, [...groups.get(k) ?? [], a]);
2073
+ }
1458
2074
  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)
2075
+ for (const [question, members] of groups) {
2076
+ const split = targetGroupWordings(repo0(members), members[0].f, facts).some((w) => o.split?.has(w));
2077
+ if (members.length === 1 || split) {
2078
+ for (const m of members)
2079
+ out.push({ ...own.get(m.f.id), factIds: [m.f.id] });
1462
2080
  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(" ");
2081
+ }
2082
+ const repo = members[0].f.repo;
2083
+ const whats = [...new Set(members.map((m) => callWhat(m.f)))];
2084
+ const listed = whats.length > 5 ? `${whats.slice(0, 5).join(", ")} and ${whats.length - 5} more` : whats.join(", ");
2085
+ const n = members.length;
2086
+ const evidence = [...new Set(members.flatMap((m) => m.q.evidence))].sort(byLine);
1470
2087
  const draft = {
1471
- question: q.question,
2088
+ question,
1472
2089
  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
- })),
2090
+ ...offered(members, facts, records, op, asks, ctx).map((s) => suggestionOption(s, repo, n, targetPathPrefix(members[0].f.path))),
2091
+ anotherOption(op),
2092
+ {
2093
+ label: TARGETS_SPLIT,
2094
+ means: `The ${n} calls (${listed}) don't all go to one place`,
2095
+ changes: "Nothing is joined from this answer; each call is asked on its own when the facts are next written"
2096
+ },
1483
2097
  {
1484
2098
  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"
2099
+ means: "Nothing in the estate or the org pack's systems answers any of them",
2100
+ changes: "The calls stay loose ends, and this isn't asked again"
1487
2101
  }
1488
2102
  ],
1489
- evidence: q.evidence,
2103
+ evidence,
1490
2104
  scope: "repo"
1491
2105
  };
1492
2106
  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,
2107
+ factId: members[0].f.id,
2108
+ factIds: members.map((m) => m.f.id),
2109
+ id: questionId({ step: TARGETS_STEP, repo, question, evidence }),
2110
+ repo,
1501
2111
  draft
1502
2112
  });
1503
2113
  }
2114
+ return out.sort((a, b) => a.factId.localeCompare(b.factId));
2115
+ }
2116
+ function targetRuleOffers(questions, factsOf) {
2117
+ const byKey = /* @__PURE__ */ new Map();
2118
+ for (const q of questions) {
2119
+ if (q.state !== "open" || q.askedBy?.step !== TARGETS_STEP)
2120
+ continue;
2121
+ const facts = factsOf(q.repo) ?? [];
2122
+ const at3 = new Set(q.evidence);
2123
+ const calls = facts.filter((f) => f.kind === "call" && f.repo === q.repo && at3.has(`${f.cite.file}:${f.cite.line}`));
2124
+ if (!calls.length)
2125
+ continue;
2126
+ const prefixes = new Set(calls.map((c) => targetPathPrefix(c.path)));
2127
+ const hosts = new Set(calls.map((c) => c.host?.replace(/^[a-z][a-z0-9+.-]*:\/\//i, "").split(/[/:]/)[0]));
2128
+ const [prefix] = prefixes;
2129
+ const [host] = hosts;
2130
+ const match = prefixes.size === 1 && prefix !== "/" ? `${prefix}/**` : hosts.size === 1 && host ? host : void 0;
2131
+ if (!match)
2132
+ continue;
2133
+ const key = `${q.repo}
2134
+ ${match}`;
2135
+ const x = byKey.get(key) ?? { match, ids: [], calls: 0 };
2136
+ x.ids.push(q.id);
2137
+ x.calls += calls.length;
2138
+ byKey.set(key, x);
2139
+ }
2140
+ const out = /* @__PURE__ */ new Map();
2141
+ for (const x of byKey.values()) {
2142
+ if (x.ids.length < 2)
2143
+ continue;
2144
+ const ids = [...x.ids].sort();
2145
+ for (const id of ids)
2146
+ out.set(id, { match: x.match, ids, calls: x.calls });
2147
+ }
2148
+ return out;
2149
+ }
2150
+ var segmentsOf = (p) => p.split(/[?#]/)[0].split("/").filter(Boolean).map((x) => /^[{:<]/.test(x) ? "{}" : x);
2151
+ function ruleCoversCall(match, f, facts) {
2152
+ if (isPathMatch(match)) {
2153
+ const want = segmentsOf(match.replace(/\/\*\*$/, ""));
2154
+ const got = segmentsOf(f.path);
2155
+ return want.length <= got.length && want.every((x, i) => x === "{}" || got[i] === "{}" || x === got[i]);
2156
+ }
2157
+ const url = urlOf(f, facts);
2158
+ const hp = url ? urlHostPort(url) : void 0;
2159
+ return !!hp?.host && hostMatches(match, hp.host, hp.port);
2160
+ }
2161
+ function ruledTargets(repo, facts, questions, o = {}) {
2162
+ 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 || b.answer.at.localeCompare(a.answer.at) || a.id.localeCompare(b.id));
2163
+ const out = /* @__PURE__ */ new Map();
2164
+ if (!rules.length)
2165
+ return out;
2166
+ for (const f of facts) {
2167
+ if (!ruleMayName(f, facts))
2168
+ continue;
2169
+ const q = rules.find((r) => ruleCoversCall(r.answer.match, f, facts));
2170
+ const label2 = q ? answerLabel2(q) : void 0;
2171
+ if (q && label2 && label2 !== TARGETS_SPLIT)
2172
+ out.set(f.id, { id: q.id, match: q.answer.match, label: label2 });
2173
+ }
1504
2174
  return out;
1505
2175
  }
1506
- function targetsFromAnswers(repo, facts, questions) {
2176
+ function targetsFromAnswers(repo, facts, questions, o = {}) {
1507
2177
  const byId = new Map(questions.map((q) => [q.id, q]));
2178
+ const groups = /* @__PURE__ */ new Map();
2179
+ for (const q of [...questions].filter((x) => x.askedBy?.step === TARGETS_STEP && x.state === "answered" && !!x.answer && typeof x.question === "string").sort((a, b) => a.answer.at.localeCompare(b.answer.at) || a.id.localeCompare(b.id)))
2180
+ groups.set(q.question.trim(), q);
2181
+ const ruled = ruledTargets(repo, facts, questions, o);
1508
2182
  const out = {};
1509
2183
  for (const f of [...facts].sort((a, b) => a.id.localeCompare(b.id))) {
1510
2184
  if (f.kind !== "call")
@@ -1512,18 +2186,104 @@ function targetsFromAnswers(repo, facts, questions) {
1512
2186
  const q = targetQuestionFor(f);
1513
2187
  if (!q)
1514
2188
  continue;
1515
- const asked = byId.get(questionId({ step: TARGETS_STEP, repo, ...q }));
1516
- if (!asked || asked.state !== "answered" || !asked.answer)
2189
+ let asked = byId.get(questionId({ step: TARGETS_STEP, repo, ...q }));
2190
+ if (!asked || asked.state !== "answered" || !asked.answer) {
2191
+ const g = targetGroupWordings(repo, f, facts).map((w) => groups.get(w)).find((x) => !!x);
2192
+ asked = g && (needsTarget(f, facts) || g.evidence.includes(q.evidence[0])) ? g : void 0;
2193
+ }
2194
+ let label2 = asked && asked.state === "answered" && asked.answer ? answerLabel2(asked) : void 0;
2195
+ let byAi = label2 && asked?.answer && isAiBy(asked.answer.by) ? asked.answer.by : void 0;
2196
+ if (!label2 || label2 === TARGETS_SPLIT || label2 === TARGETS_ANOTHER) {
2197
+ label2 = ruled.get(f.id)?.label;
2198
+ byAi = void 0;
2199
+ }
2200
+ if (!label2 || label2 === TARGETS_SPLIT || label2 === TARGETS_ANOTHER)
1517
2201
  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)
2202
+ if (byAi && o.ai)
2203
+ o.ai[f.id] = { runtime: byAi.runtime, run: byAi.run };
2204
+ if (label2 === TARGETS_NONE) {
2205
+ out[f.id] = NO_TARGET;
1520
2206
  continue;
2207
+ }
1521
2208
  const to = slugify(label2);
1522
2209
  if (to)
1523
2210
  out[f.id] = to;
1524
2211
  }
1525
2212
  return out;
1526
2213
  }
2214
+ function answeredQuestionOfCall(repo, facts, questions, factId) {
2215
+ const f = facts.find((x) => x.id === factId);
2216
+ if (!f || f.kind !== "call")
2217
+ return void 0;
2218
+ const own = targetQuestionFor(f);
2219
+ const ownId = questionId({ step: TARGETS_STEP, repo, ...own });
2220
+ const answered = questions.filter((q2) => q2.askedBy?.step === TARGETS_STEP && q2.state === "answered" && q2.repo === repo);
2221
+ 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]));
2222
+ return q?.answer?.match ? void 0 : q;
2223
+ }
2224
+ function answerReconfirmed(q) {
2225
+ if (!q.answer || !q.reopened)
2226
+ return false;
2227
+ const now2 = answerLabel2(q);
2228
+ const was = answerLabel2({ ...q, answer: q.reopened.answer });
2229
+ return !!now2 && !!was && slugify(now2) === slugify(was);
2230
+ }
2231
+ var JOINED_BY = {
2232
+ named: "the call names it",
2233
+ repo: "the call names its repo",
2234
+ host: "the manifest's hosts and base paths decide it",
2235
+ topic: "it listens on the call's topic"
2236
+ };
2237
+ function callsOfTargetQuestion(repo, facts, q) {
2238
+ return facts.filter((f) => {
2239
+ if (f.kind !== "call")
2240
+ return false;
2241
+ const own = targetQuestionFor(f);
2242
+ if (questionId({ step: TARGETS_STEP, repo, ...own }) === q.id)
2243
+ return true;
2244
+ return targetGroupWordings(repo, f, facts).includes(q.question.trim()) && q.evidence.includes(own.evidence[0]);
2245
+ });
2246
+ }
2247
+ function staleTargetQuestions(input) {
2248
+ const drafted = new Set(input.drafted.map((d) => d.id));
2249
+ const out = [];
2250
+ for (const q of input.open) {
2251
+ if (q.state !== "open" || q.askedBy.step !== TARGETS_STEP || drafted.has(q.id))
2252
+ continue;
2253
+ const calls = callsOfTargetQuestion(input.repo, input.facts, q);
2254
+ if (!calls.length)
2255
+ continue;
2256
+ const ids = new Set(calls.map((c) => c.id));
2257
+ const by = input.drafted.filter((d) => d.factIds.some((x) => ids.has(x))).map((d) => d.id);
2258
+ if (by.length) {
2259
+ out.push({ id: q.id, why: `its calls are asked now in ${by.join(", ")}`, replacedBy: by });
2260
+ continue;
2261
+ }
2262
+ if (calls.some((c) => input.asked.has(c.id)))
2263
+ continue;
2264
+ const reasons = calls.map((c) => {
2265
+ const rule = input.ruled?.get(c.id);
2266
+ if (rule)
2267
+ return `${callWhat(c)} is answered by rule in ${rule.id} (${rule.match})`;
2268
+ const j = input.join?.joins.find((x) => x.call.repo === input.repo && x.call.fact === c.id && x.how !== "answer");
2269
+ if (j && !isGuessedJoin(j) && JOINED_BY[j.how])
2270
+ return `${callWhat(c)} joins ${j.entry.component} in ${j.entry.repo}: ${JOINED_BY[j.how]}`;
2271
+ if (!j && c.to)
2272
+ return `${callWhat(c)} now names ${c.to} in the code`;
2273
+ return void 0;
2274
+ });
2275
+ const rules = [
2276
+ ...new Set(calls.flatMap((c) => input.ruled?.get(c.id) ? [input.ruled.get(c.id).id] : []))
2277
+ ].sort();
2278
+ if (reasons.every((r) => r !== void 0))
2279
+ out.push({
2280
+ id: q.id,
2281
+ why: reasons.join("; "),
2282
+ ...rules.length ? { replacedBy: rules } : {}
2283
+ });
2284
+ }
2285
+ return out.sort((a, b) => a.id.localeCompare(b.id));
2286
+ }
1527
2287
  function componentRenames(repos, op, taken = []) {
1528
2288
  const manifests = estateRepos(op);
1529
2289
  const users = /* @__PURE__ */ new Map();
@@ -1566,7 +2326,7 @@ function factsRecords(input) {
1566
2326
  const facts = withComponents(repo, input.facts, op, input.renames).map((f) => {
1567
2327
  let g = f;
1568
2328
  const to = input.targets?.[g.id];
1569
- if (g.kind === "call" && !g.to && to)
2329
+ if (g.kind === "call" && !g.to && to && to !== NO_TARGET)
1570
2330
  g = { ...g, to };
1571
2331
  return g;
1572
2332
  });
@@ -1613,20 +2373,33 @@ function factsRecords(input) {
1613
2373
  return { facts, projected, factById, newRecords, newFlows, loose, looseFact };
1614
2374
  }
1615
2375
  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();
2376
+ return outsideProfileSystems(op).flatMap((s) => [s.id, s.name, ...s.aliases]).sort();
2377
+ }
2378
+ function outsideProfileSystems(op) {
2379
+ const own = new Set(estateRepos(op).flatMap((r) => r.system ? [r.system] : []));
2380
+ const seen = /* @__PURE__ */ new Set();
2381
+ 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
2382
  }
2383
+ 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
2384
  function joinReposOf(op, list, renames) {
1619
2385
  const manifests = estateRepos(op);
1620
2386
  return list.map(({ repo, facts }) => {
1621
2387
  const m = manifests.find((r) => r.id === repo);
1622
- const modules = (m?.modules ?? []).filter((x) => x.hosts.length > 0);
2388
+ 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
2389
  const component = m?.component ? renames?.[repo]?.[factIdPart(m.component)] ?? m.component : void 0;
1624
2390
  return {
1625
2391
  repo,
1626
2392
  facts: renames?.[repo] ? withComponents(repo, facts, op, renames) : facts,
1627
2393
  ...component ? { component } : {},
1628
2394
  ...m?.hosts.length ? { hosts: m.hosts } : {},
1629
- ...modules.length ? { modules: modules.map((x) => ({ component: x.component, hosts: x.hosts })) } : {},
2395
+ ...m?.basePath ? { basePath: m.basePath } : {},
2396
+ ...modules.length ? {
2397
+ modules: modules.map((x) => ({
2398
+ component: x.component,
2399
+ hosts: x.hosts,
2400
+ ...x.basePath ? { basePath: x.basePath } : {}
2401
+ }))
2402
+ } : {},
1630
2403
  // An API gateway's entries are routes the join follows through (ADR 0124 addendum A-gateway).
1631
2404
  // A type that says which side it faces (`gateway-external`, `gateway-internal`) marks each
1632
2405
  // hop through it (A-gateway2); a plain `gateway` leaves that to the proxy's virtual host.
@@ -1668,12 +2441,7 @@ function applyFactJoins(records, join10, repos, withCallers) {
1668
2441
  } else if (!(r.kind === "FieldMapping" && ifaces.has(r.interface)))
1669
2442
  kept2.push(r);
1670
2443
  }
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
- };
2444
+ const re = remapIds(was);
1677
2445
  const joined = joinRecords(join10, repos);
1678
2446
  const callers = withCallers ? callerRecords(withCallers.callers, repos, withCallers.join, {
1679
2447
  ...withCallers.only ? { only: withCallers.only } : {},
@@ -1694,20 +2462,28 @@ function applyFactJoins(records, join10, repos, withCallers) {
1694
2462
  }
1695
2463
  var entryText = (j) => j.method ? `${j.method} ${j.path}` : j.path;
1696
2464
  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));
2465
+ 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
2466
+ // written again with it, so a changed value moves the link.
2467
+ [...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).
2468
+ [...j.exposedBy ?? [], ...(j.gateway ?? []).flatMap((h) => h.exposedBy ?? [])].some((t) => t.repo === repo));
1698
2469
  const ifaces = new Set(joins.map((j) => j.interface));
2470
+ const outside = (all.outside ?? []).filter((o) => o.call.repo === repo);
2471
+ const exposure = (all.exposure ?? []).filter((x) => x.exposure.repo === repo || "repo" in x && x.repo === repo);
1699
2472
  return {
1700
2473
  joins,
1701
2474
  asks: all.asks.filter((a) => a.call.repo === repo),
1702
2475
  misses: all.misses.filter((m) => m.call.repo === repo),
2476
+ ...outside.length ? { outside } : {},
2477
+ ...exposure.length ? { exposure } : {},
1703
2478
  lineage: all.lineage.filter((l) => ifaces.has(l.interface)),
1704
2479
  gaps: all.gaps.filter((g) => ifaces.has(g.interface))
1705
2480
  };
1706
2481
  }
1707
- function catalogWithFacts(input) {
1708
- const { repo, op } = input;
2482
+ function catalogWithFacts(given) {
2483
+ const { repo, op } = given;
1709
2484
  if (isPrototypeRepo(op, repo))
1710
- return prototypeLeftOut(input);
2485
+ return prototypeLeftOut(given);
2486
+ const input = { ...given, existing: stripSharedMaps(stripLinkEvidence(given.existing)) };
1711
2487
  const others = (input.estate ?? []).filter((x) => x.repo !== repo && !isPrototypeRepo(op, x.repo));
1712
2488
  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
2489
  const { facts, projected, newRecords, newFlows, looseFact } = factsRecords({
@@ -1757,20 +2533,46 @@ function catalogWithFacts(input) {
1757
2533
  const remap = /* @__PURE__ */ new Map();
1758
2534
  const stepTo = /* @__PURE__ */ new Map();
1759
2535
  let join10;
2536
+ let answerMisses;
2537
+ let estateJoin;
2538
+ let terms;
1760
2539
  let records0 = [...kept2, ...newRecords];
1761
2540
  if (input.estate) {
1762
2541
  const repos = joinReposOf(op, [{ repo, facts }, ...others], renames);
1763
2542
  const whole = joinFacts({
1764
2543
  repos,
1765
2544
  targets: { ...input.estateTargets, [repo]: input.targets ?? {} },
1766
- outside: outsideSystems(op)
2545
+ pairs: { ...input.estatePairs, [repo]: input.pairs ?? {} },
2546
+ aiTargets: { ...input.estateAiTargets, [repo]: input.aiTargets ?? {} },
2547
+ outside: outsideSystems(op),
2548
+ ...joinConventions(op),
2549
+ // G4: the walk through gateways is a stack helper (ATTICUS_HELPERS_OFF=gateway-walk).
2550
+ walkGateways: helperOn("gateway-walk")
1767
2551
  });
1768
2552
  join10 = joinAbout(whole, repo);
2553
+ estateJoin = { whole, repos };
2554
+ const named2 = { ...input.estateTargets, [repo]: input.targets ?? {} };
2555
+ const mine2 = /* @__PURE__ */ new Set([
2556
+ factIdPart(repo),
2557
+ ...repos.filter((r) => r.repo === repo).flatMap((r) => [r.component, ...(r.modules ?? []).map((m) => m.component)]).filter((x) => !!x).map(factIdPart),
2558
+ ...facts.flatMap((f) => f.component ? [factIdPart(f.component)] : [])
2559
+ ]);
2560
+ answerMisses = whole.misses.filter((m) => {
2561
+ const t = named2[m.call.repo]?.[m.call.fact];
2562
+ return !!t && t !== NO_TARGET && (m.call.repo === repo || mine2.has(factIdPart(m.target)));
2563
+ });
1769
2564
  records0 = applyFactJoins(records0, join10, repos, {
1770
- callers: joinCallers({ repos, join: whole, declared: declaredCallsOf(op.profiles) }),
2565
+ callers: joinCallers({
2566
+ repos,
2567
+ join: whole,
2568
+ declared: declaredCallsOf(op.profiles),
2569
+ ...joinConventions(op)
2570
+ }),
1771
2571
  join: whole,
1772
2572
  only: [repo]
1773
2573
  });
2574
+ terms = joinTerms({ repos, join: whole, bundleFolders: joinConventions(op).bundleFolders });
2575
+ records0 = [...records0, ...termGrantRecords(terms, repos, { only: [repo] })];
1774
2576
  for (const j of join10.joins) {
1775
2577
  const id = j.call.repo === repo ? projected.ids[j.call.fact] : void 0;
1776
2578
  if (!id || remap.has(id))
@@ -1779,7 +2581,10 @@ function catalogWithFacts(input) {
1779
2581
  stepTo.set(id, { component: j.entry.component, entry: entryText(j) });
1780
2582
  }
1781
2583
  }
1782
- const merged0 = mergeEstate(op, estateRepos(op), records0.sort(byKindId));
2584
+ const merged0 = mergeEstate(op, estateRepos(op), records0.sort(byKindId), void 0, /* @__PURE__ */ new Set([
2585
+ repo,
2586
+ ...input.estate ? others.filter((r) => r.facts.length > 0).map((r) => r.repo) : []
2587
+ ]));
1783
2588
  const heard = new Set((join10?.joins ?? []).filter((j) => j.how === "topic").map((j) => `${j.entry.repo}|${j.entry.at}|${j.entry.component}`));
1784
2589
  const merged = {
1785
2590
  ...merged0,
@@ -1806,15 +2611,7 @@ function catalogWithFacts(input) {
1806
2611
  joinedTo.set(id, iface.to);
1807
2612
  }
1808
2613
  }
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
- };
2614
+ const re = remapIds(remap);
1818
2615
  const records = merged.records.map((r) => {
1819
2616
  if (r.kind === "Schema")
1820
2617
  return { ...r, id: re(r.id), ref: re(r.ref) };
@@ -1824,7 +2621,25 @@ function catalogWithFacts(input) {
1824
2621
  return { ...r, interface: re(r.interface) };
1825
2622
  return r;
1826
2623
  });
1827
- const applied = applyCurated([...records].sort(byKindId), input.decisions);
2624
+ const curated = applyCurated([...records].sort(byKindId), input.decisions);
2625
+ const linkEvidence = estateJoin ? linkEvidenceOf({
2626
+ join: estateJoin.whole,
2627
+ repos: estateJoin.repos,
2628
+ contracts: input.linkEvidence?.contracts ?? [],
2629
+ traffic: input.linkEvidence?.traffic ?? []
2630
+ }) : void 0;
2631
+ const evidenced = linkEvidence ? { ...curated, records: applyLinkEvidence(curated.records, linkEvidence.marks) } : curated;
2632
+ const systems = outsideProfileSystems(op);
2633
+ const sharedWhole = estateJoin && input.shared?.length ? joinSharedMaps({
2634
+ outside: estateJoin.whole.outside ?? [],
2635
+ repos: estateJoin.repos,
2636
+ maps: input.shared,
2637
+ systems
2638
+ }) : void 0;
2639
+ const applied = {
2640
+ ...evidenced,
2641
+ records: sharedWhole ? applySharedMaps(evidenced.records, sharedWhole, systems) : stripSharedMaps(evidenced.records)
2642
+ };
1828
2643
  const newIds = new Set(applied.records.filter((r) => r.evidence.some(mine)).map((r) => r.id));
1829
2644
  const oldIds = new Set([...dropped].map((k) => k.slice(k.indexOf("|") + 1)));
1830
2645
  const flows = newFlows.map((f) => {
@@ -1865,7 +2680,11 @@ function catalogWithFacts(input) {
1865
2680
  projectionIssues: projected.issues.filter((i) => i.code !== "unresolved-target"),
1866
2681
  invalid: merged.invalid,
1867
2682
  notes: merged.notes,
1868
- ...join10 ? { join: join10 } : {}
2683
+ ...join10 ? { join: join10 } : {},
2684
+ ...linkEvidence ? { linkEvidence } : {},
2685
+ ...sharedWhole ? { shared: sharedJoinAbout(sharedWhole, repo) } : {},
2686
+ ...terms ? { terms } : {},
2687
+ ...answerMisses?.length ? { answerMisses } : {}
1869
2688
  };
1870
2689
  }
1871
2690
  function prototypeLeftOut(input) {
@@ -1888,16 +2707,99 @@ function prototypeLeftOut(input) {
1888
2707
  ]
1889
2708
  };
1890
2709
  }
2710
+ function estateFromFacts(input) {
2711
+ const repos = input.repos.filter((x) => !isPrototypeRepo(input.op, x.repo));
2712
+ let records = [...input.existing ?? []];
2713
+ const specs = [];
2714
+ const invalid = [];
2715
+ for (const { repo, facts } of repos) {
2716
+ const out = catalogWithFacts({
2717
+ repo,
2718
+ facts,
2719
+ existing: records,
2720
+ op: input.op,
2721
+ decisions: input.decisions ?? [],
2722
+ estate: repos.filter((x) => x.repo !== repo)
2723
+ });
2724
+ records = out.records;
2725
+ specs.push({ repo, flows: out.flows });
2726
+ invalid.push(...out.invalid);
2727
+ }
2728
+ const renames = componentRenames(repos, input.op);
2729
+ const join10 = joinFacts({
2730
+ repos: joinReposOf(input.op, repos.map(({ repo, facts }) => ({
2731
+ repo,
2732
+ facts: factsRecords({ repo, facts, op: input.op, renames }).facts
2733
+ }))),
2734
+ outside: outsideSystems(input.op),
2735
+ ...joinConventions(input.op),
2736
+ walkGateways: helperOn("gateway-walk")
2737
+ });
2738
+ return { records, specs, join: join10, invalid };
2739
+ }
2740
+ function answerRuleOffers(input) {
2741
+ const ruled = input.questions.filter((q) => q.askedBy?.step === TARGETS_STEP && q.state === "answered" && !!q.answer?.match);
2742
+ const groups = /* @__PURE__ */ new Map();
2743
+ for (const r of input.repos)
2744
+ for (const f of r.facts) {
2745
+ if (f.kind !== "call")
2746
+ continue;
2747
+ const q = answeredQuestionOfCall(r.repo, r.facts, input.questions, f.id);
2748
+ const label2 = q ? answerLabel2(q) : void 0;
2749
+ if (!q || !label2 || label2 === TARGETS_NONE || label2 === TARGETS_SPLIT || label2 === TARGETS_ANOTHER)
2750
+ continue;
2751
+ const url = urlOf(f, r.facts);
2752
+ const host = url ? urlHostPort(url)?.host : void 0;
2753
+ const via = host ? `on ${host.toLowerCase()}` : f.base ? `through ${f.base}` : void 0;
2754
+ if (!via)
2755
+ continue;
2756
+ groups.set(via, [
2757
+ ...groups.get(via) ?? [],
2758
+ { repo: r.repo, q, label: label2, f, facts: r.facts, ...r.type ? { type: r.type } : {} }
2759
+ ]);
2760
+ }
2761
+ const out = [];
2762
+ for (const [via, xs] of [...groups].sort(([a], [b]) => a.localeCompare(b))) {
2763
+ const repos = [...new Set(xs.map((x) => x.repo))].sort();
2764
+ const labels = new Set(xs.map((x) => slugify(x.label)));
2765
+ if (repos.length < 2 || labels.size !== 1)
2766
+ continue;
2767
+ if (xs.every((x) => ruled.some((q) => ruleCoversCall(q.answer.match, x.f, x.facts))))
2768
+ continue;
2769
+ const label2 = xs[0].label;
2770
+ const prefixes = new Set(xs.map((x) => targetPathPrefix(x.f.path)));
2771
+ const prefix = prefixes.size === 1 ? [...prefixes][0] : void 0;
2772
+ const match = via.startsWith("on ") ? via.slice(3) : prefix && prefix !== "/" ? `${prefix}/**` : void 0;
2773
+ const types = new Set(xs.map((x) => x.type));
2774
+ const type = types.size === 1 ? xs[0].type : void 0;
2775
+ const questions = [...new Set(xs.map((x) => x.q.id))].sort();
2776
+ const first = xs.find((x) => x.q.id === questions[0]).q;
2777
+ const n = first.options.findIndex((o) => o.label === label2);
2778
+ const pick = n >= 0 ? `--option ${n + 1}` : `--text "${label2}"`;
2779
+ if (!match || !type)
2780
+ continue;
2781
+ const command = `atticus questions answer ${first.id} ${pick} --match "${match}" --scope repo-type --type ${type}`;
2782
+ out.push({
2783
+ via,
2784
+ label: label2,
2785
+ repos,
2786
+ questions,
2787
+ match,
2788
+ type,
2789
+ command,
2790
+ 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}.`
2791
+ });
2792
+ }
2793
+ return out;
2794
+ }
1891
2795
 
1892
2796
  // packages/scanner/dist/facts-scan.js
1893
- import { readFile as readFile4 } from "node:fs/promises";
1894
- import { join as join6 } from "node:path";
1895
2797
  async function factsForScan(root, op, repo, commit, readerRecords, at3 = {}) {
1896
2798
  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() === "")
2799
+ const text2 = await readFile4(join6(factsDirOf(root), `${repo}.jsonl`), "utf8").catch(() => void 0);
2800
+ if (text2 === void 0 || text2.trim() === "")
1899
2801
  return {};
1900
- const parsed = parseFacts(text);
2802
+ const parsed = parseFacts(text2);
1901
2803
  if (!parsed.ok)
1902
2804
  return {
1903
2805
  note: `${repo}: ${rel} doesn't read as facts (${parsed.issues[0]?.message ?? "invalid"}); its readers' records are used. Run atticus verify ${repo}.`
@@ -1910,29 +2812,45 @@ async function factsForScan(root, op, repo, commit, readerRecords, at3 = {}) {
1910
2812
  };
1911
2813
  const older = commit ? facts.filter((f) => !sameCommit(f.commit, commit)) : [];
1912
2814
  let carriedSaid = "";
2815
+ let unread = [];
1913
2816
  if (older.length) {
1914
2817
  const diff = at3.dir ? await changesSince(at3.dir, older.map((f) => f.commit), commit, at3.git) : void 0;
1915
2818
  if (!diff || diff.missing.length)
1916
2819
  return {
1917
2820
  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
2821
  };
2822
+ const off = stackHelpersOff();
1919
2823
  const report = await verifyFacts({
1920
2824
  facts,
1921
2825
  source: dirSource(at3.dir),
2826
+ ...off.has("derived-names") ? { derivedNames: false } : {},
2827
+ ...off.has("readers") ? { readers: false } : {},
1922
2828
  commit,
1923
- carry: diff.changes
2829
+ carry: diff.changes,
2830
+ conventions: conventionsOf(op),
2831
+ // ADR 0180: a type taken out of the organisation's set after a read is flagged here too.
2832
+ componentTypes: new Set(effectiveComponentTypes(op.componentTypes).map((t) => t.id))
1924
2833
  });
1925
2834
  const from = [...new Set(older.map((f) => f.commit.slice(0, 7)))].join(", ");
1926
2835
  if (!report.accepted.length)
1927
2836
  return {
1928
2837
  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
2838
  };
2839
+ unread = rereadPlan({
2840
+ facts,
2841
+ commit,
2842
+ carry: diff.changes,
2843
+ source: dirSource(at3.dir),
2844
+ conventions: conventionsOf(op)
2845
+ }).files.map((f) => f.path);
1930
2846
  facts = report.accepted;
1931
2847
  const left = report.rejected.length;
1932
2848
  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
2849
  }
1934
- const questions = (await listQuestions(root, { repo }).catch(() => ({ questions: [] }))).questions;
1935
- const targets = targetsFromAnswers(repo, facts, questions);
2850
+ const questions = (await listQuestions(root, { step: TARGETS_STEP }).catch(() => ({ questions: [] }))).questions;
2851
+ const targets = targetsFromAnswers(repo, facts, questions, {
2852
+ repoType: estateRepos(op).find((r) => r.id === repo)?.type
2853
+ });
1936
2854
  const others = (await estateFacts(root, op, repo)).repos;
1937
2855
  const renames = componentRenames([{ repo, facts }, ...others], op);
1938
2856
  const { newRecords, facts: used } = factsRecords({ repo, facts, op, targets, renames });
@@ -1963,35 +2881,52 @@ async function factsForScan(root, op, repo, commit, readerRecords, at3 = {}) {
1963
2881
  records: newRecords,
1964
2882
  facts: used,
1965
2883
  targets,
2884
+ basis: { from: "facts", unread },
1966
2885
  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
2886
  };
1968
2887
  }
2888
+ async function pairsOfRepo(root, repo, facts) {
2889
+ const text2 = await readFile4(join6(factsDirOf(root), `${repo}.corrections.jsonl`), "utf8").catch(() => void 0);
2890
+ if (!text2 || !text2.includes('"link"'))
2891
+ return {};
2892
+ return pairsFromCorrections(facts, parseCorrections(text2).corrections);
2893
+ }
1969
2894
  async function estateFacts(root, op, except) {
1970
2895
  const repos = [];
1971
2896
  const targets = {};
2897
+ const aiTargets = {};
2898
+ const pairs = {};
1972
2899
  const notes = [];
2900
+ const unverified = [];
2901
+ const questions = (await listQuestions(root, { step: TARGETS_STEP }).catch(() => ({ questions: [] }))).questions;
1973
2902
  for (const r of productionRepos(op)) {
1974
2903
  if (r.id === except)
1975
2904
  continue;
1976
- const text = await readFile4(join6(factsDirOf(root), `${r.id}.jsonl`), "utf8").catch(() => void 0);
1977
- if (text === void 0 || text.trim() === "")
2905
+ const text2 = await readFile4(join6(factsDirOf(root), `${r.id}.jsonl`), "utf8").catch(() => void 0);
2906
+ if (text2 === void 0 || text2.trim() === "")
1978
2907
  continue;
1979
- const parsed = parseFacts(text);
2908
+ const parsed = parseFacts(text2);
1980
2909
  if (!parsed.ok) {
1981
2910
  notes.push(`${r.id}: its facts don't read, so the estate join leaves them out.`);
1982
2911
  continue;
1983
2912
  }
1984
2913
  if (parsed.facts.some((f) => f.verified === "none")) {
2914
+ unverified.push(r.id);
1985
2915
  notes.push(`${r.id}: some of its facts aren't verified yet, so the estate join leaves them out (atticus verify ${r.id}).`);
1986
2916
  continue;
1987
2917
  }
1988
2918
  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);
2919
+ const ai = {};
2920
+ const named2 = targetsFromAnswers(r.id, parsed.facts, questions, { repoType: r.type, ai });
1991
2921
  if (Object.keys(named2).length)
1992
2922
  targets[r.id] = named2;
2923
+ if (Object.keys(ai).length)
2924
+ aiTargets[r.id] = ai;
2925
+ const paired = await pairsOfRepo(root, r.id, parsed.facts);
2926
+ if (Object.keys(paired).length)
2927
+ pairs[r.id] = paired;
1993
2928
  }
1994
- return { repos, targets, notes };
2929
+ return { repos, targets, pairs, notes, unverified, aiTargets };
1995
2930
  }
1996
2931
 
1997
2932
  // packages/scanner/dist/scan-settings.js
@@ -2006,10 +2941,10 @@ async function citedLine(repo, at3) {
2006
2941
  const i = at3.lastIndexOf(":");
2007
2942
  const path = at3.slice(0, i);
2008
2943
  const n = Number(at3.slice(i + 1));
2009
- const text = await repo.readText(path).catch(() => void 0);
2010
- if (text === void 0)
2944
+ const text2 = await repo.readText(path).catch(() => void 0);
2945
+ if (text2 === void 0)
2011
2946
  return void 0;
2012
- return text.replace(/\r\n?/g, "\n").split("\n")[n - 1];
2947
+ return text2.replace(/\r\n?/g, "\n").split("\n")[n - 1];
2013
2948
  }
2014
2949
  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
2950
  var NOT_OWN = /(^|\/)(node_modules|\.git|target|bin|obj|dist|build|\.atticus|vendor)\//;
@@ -2068,8 +3003,8 @@ async function checkScanSettings(s, repo) {
2068
3003
  if (!(isConfigFile(file) && names)) {
2069
3004
  const dropped = code.filter((f) => re.test(f));
2070
3005
  const root = mainRoots.find((d) => {
2071
- const under = code.filter((f) => f.startsWith(`${d}/`));
2072
- return under.length && under.every((f) => re.test(f));
3006
+ const under2 = code.filter((f) => f.startsWith(`${d}/`));
3007
+ return under2.length && under2.every((f) => re.test(f));
2073
3008
  });
2074
3009
  const why = " so it is still read; only a build or config file that leaves it out itself can say so";
2075
3010
  if (dropped.length * 2 > code.length) {
@@ -2126,9 +3061,9 @@ async function namesNotIn(repo, paths, names) {
2126
3061
  for await (const f of repo.listFiles(["**/*"])) {
2127
3062
  if (!paths.some((x) => x.test(f)))
2128
3063
  continue;
2129
- const text = await repo.readText(f).catch(() => "");
3064
+ const text2 = await repo.readText(f).catch(() => "");
2130
3065
  for (const n of [...left])
2131
- if (text.includes(n))
3066
+ if (text2.includes(n))
2132
3067
  left.delete(n);
2133
3068
  if (left.size === 0)
2134
3069
  break;
@@ -2355,54 +3290,6 @@ function withPluginScan(scan, extra) {
2355
3290
  };
2356
3291
  }
2357
3292
 
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
3293
  // packages/scanner/dist/scan.js
2407
3294
  var STRUCTURED = /\.(ya?ml|json|xml|tf|tfvars|properties|toml|conf|hcl)$/i;
2408
3295
  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 +3305,6 @@ async function orgPackDirFor(root) {
2418
3305
  return { dir: join9(root, ".atticus", "org-pack"), adopted: true };
2419
3306
  return { dir: join9(root, "org-pack"), adopted: false };
2420
3307
  }
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
3308
  async function federate(local, sources, problems, root) {
2429
3309
  const have = new Set(local.map((r) => `${r.kind}/${r.id}`));
2430
3310
  const records = [];
@@ -2553,7 +3433,8 @@ async function scanEstate(o) {
2553
3433
  }));
2554
3434
  const brokenDialects = /* @__PURE__ */ new Set();
2555
3435
  const dialects = await usableDialects(op, orgPackDir, problems, brokenDialects);
2556
- const frameworks = await usableFrameworks(op, problems);
3436
+ const off = stackHelpersOff();
3437
+ const frameworks = off.has("framework-models") ? [] : await usableFrameworks(op, problems);
2557
3438
  const pluginSet = await usablePlugins(op);
2558
3439
  problems.push(...pluginSet.problems);
2559
3440
  const byName = new Map(dialects.map((d) => [d.name, d]));
@@ -2563,6 +3444,7 @@ async function scanEstate(o) {
2563
3444
  const repos = productionRepos(op, o.repos);
2564
3445
  const results = [];
2565
3446
  const factNotes = prototypeScanNotes(op, o.repos);
3447
+ const basis = {};
2566
3448
  const factRepos = [];
2567
3449
  const structuredOf = /* @__PURE__ */ new Map();
2568
3450
  const modulesOf = /* @__PURE__ */ new Map();
@@ -2570,7 +3452,7 @@ async function scanEstate(o) {
2570
3452
  let done = 0;
2571
3453
  for (const r of repos) {
2572
3454
  throwIfCancelled();
2573
- const dir = o.repoDir?.(r.id) ?? inPlace(o.root, r.url) ?? baseClonePath(o.root, r.id);
3455
+ const dir = o.repoDir?.(r.id) ?? inPlaceDir(o.root, r.url) ?? baseClonePath(o.root, r.id);
2574
3456
  let state;
2575
3457
  if (!o.repoState && !await stat(dir).catch(() => void 0)) {
2576
3458
  problems.push({
@@ -2590,7 +3472,7 @@ async function scanEstate(o) {
2590
3472
  });
2591
3473
  continue;
2592
3474
  }
2593
- const extractors = [
3475
+ const extractors = off.has("readers") ? [] : [
2594
3476
  ...BUILT_IN_EXTRACTORS,
2595
3477
  ...o.extractors ?? [],
2596
3478
  ...dialectExtractors.filter(({ d }) => r.dialects.includes(d.name) || d.appliesTo.repoTypes.includes(r.type) || // "*": any repo where the marker matches (ADR 0062).
@@ -2670,6 +3552,7 @@ async function scanEstate(o) {
2670
3552
  });
2671
3553
  if (fromFacts.note)
2672
3554
  factNotes.push(fromFacts.note);
3555
+ basis[r.id] = fromFacts.records && fromFacts.basis ? fromFacts.basis : { from: "readers" };
2673
3556
  if (fromFacts.facts)
2674
3557
  factRepos.push({ repo: r.id, facts: fromFacts.facts, targets: fromFacts.targets ?? {} });
2675
3558
  results.push({ ...scan, records: fromFacts.records ?? readerRecords });
@@ -2682,10 +3565,17 @@ async function scanEstate(o) {
2682
3565
  const join10 = joinFacts({
2683
3566
  repos: joinRepos,
2684
3567
  targets: Object.fromEntries(factRepos.map((x) => [x.repo, x.targets])),
2685
- outside: outsideSystems(op)
3568
+ outside: outsideSystems(op),
3569
+ ...joinConventions(op),
3570
+ walkGateways: helperOn("gateway-walk")
2686
3571
  });
2687
3572
  codeRecords = applyFactJoins(codeRecords, join10, joinRepos, {
2688
- callers: joinCallers({ repos: joinRepos, join: join10, declared: declaredCallsOf(op.profiles) }),
3573
+ callers: joinCallers({
3574
+ repos: joinRepos,
3575
+ join: join10,
3576
+ declared: declaredCallsOf(op.profiles),
3577
+ ...joinConventions(op)
3578
+ }),
2689
3579
  join: join10
2690
3580
  });
2691
3581
  }
@@ -2800,7 +3690,8 @@ async function scanEstate(o) {
2800
3690
  ...inputs ? { inputs } : {},
2801
3691
  curated: { applied: applied.applied, suppressed: applied.suppressed },
2802
3692
  notes,
2803
- ...federated ? { federated } : {}
3693
+ ...federated ? { federated } : {},
3694
+ basis
2804
3695
  };
2805
3696
  }
2806
3697
 
@@ -2836,8 +3727,8 @@ var RepoFiles = class {
2836
3727
  }
2837
3728
  async linesOf(path) {
2838
3729
  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"));
3730
+ const text2 = await this.repo.readText(path).catch(() => void 0);
3731
+ this.lines.set(path, text2 === void 0 ? null : text2.replace(/\r\n?/g, "\n").split("\n"));
2841
3732
  }
2842
3733
  return this.lines.get(path);
2843
3734
  }
@@ -2913,13 +3804,13 @@ async function scoreType(t, files) {
2913
3804
  };
2914
3805
  }
2915
3806
  function typeFromWords(description, types) {
2916
- const text = (description ?? "").toLowerCase();
2917
- if (!text)
3807
+ const text2 = (description ?? "").toLowerCase();
3808
+ if (!text2)
2918
3809
  return void 0;
2919
3810
  const hits = [];
2920
3811
  for (const t of types)
2921
3812
  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))
3813
+ if (words2.length > 2 && new RegExp(`(^|[^a-z0-9])${escapeRe(words2)}([^a-z0-9]|$)`).test(text2))
2923
3814
  hits.push({ type: t.id, words: words2 });
2924
3815
  const ids = new Set(hits.map((h) => h.type));
2925
3816
  if (ids.size !== 1)
@@ -2975,8 +3866,8 @@ async function readRule(rule, path, files) {
2975
3866
  const key = rule.key;
2976
3867
  if (/\.ya?ml$/i.test(path)) {
2977
3868
  const counter = new import_yaml3.LineCounter();
2978
- const text = lines.join("\n");
2979
- for (const doc of (0, import_yaml3.parseAllDocuments)(text, { lineCounter: counter })) {
3869
+ const text2 = lines.join("\n");
3870
+ for (const doc of (0, import_yaml3.parseAllDocuments)(text2, { lineCounter: counter })) {
2980
3871
  if (!("getIn" in doc))
2981
3872
  continue;
2982
3873
  for (const node of [doc.getIn(key.split("."), true), doc.get(key, true)]) {
@@ -3015,8 +3906,8 @@ async function resolveSettingsTemplate(t, repo, repoId) {
3015
3906
  out.exclude.push({ path: e.path, why: e.why, at: `${first}:1` });
3016
3907
  }
3017
3908
  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) {
3909
+ const candidates2 = (await files.matching(rule.files)).sort((a, b) => a.length - b.length || a.localeCompare(b));
3910
+ for (const f of candidates2) {
3020
3911
  const hit = await readRule(rule, f, files);
3021
3912
  if (!hit)
3022
3913
  continue;
@@ -3049,15 +3940,15 @@ var BUILD_FILES = [
3049
3940
  ["composer.json", "composer.json"]
3050
3941
  ].map(([name, glob]) => ({ name, glob, re: globToRegExp(`**/${glob}`) }));
3051
3942
  var PACKAGE_NAME = /^[A-Za-z0-9_.@/-]+$/;
3052
- function declaredDependencies(path, text) {
3943
+ function declaredDependencies(path, text2) {
3053
3944
  const out = [];
3054
3945
  const all = (re, g = 1) => {
3055
- for (const m of text.matchAll(re))
3946
+ for (const m of text2.matchAll(re))
3056
3947
  if (m[g])
3057
3948
  out.push(m[g].trim());
3058
3949
  };
3059
3950
  if (path.endsWith("pom.xml")) {
3060
- for (const block2 of text.matchAll(/<(parent|dependency|plugin)>([\s\S]*?)<\/\1>/g)) {
3951
+ for (const block2 of text2.matchAll(/<(parent|dependency|plugin)>([\s\S]*?)<\/\1>/g)) {
3061
3952
  const a = /<artifactId>\s*([^<\s]+)\s*<\/artifactId>/.exec(block2[2]);
3062
3953
  if (a)
3063
3954
  out.push(a[1]);
@@ -3066,7 +3957,7 @@ function declaredDependencies(path, text) {
3066
3957
  all(/['"]([\w.-]+):([\w.-]+)(?::[^'"]*)?['"]/g, 2);
3067
3958
  } else if (path.endsWith("package.json")) {
3068
3959
  try {
3069
- const j = JSON.parse(text);
3960
+ const j = JSON.parse(text2);
3070
3961
  const deps = Object.keys(j.dependencies ?? {});
3071
3962
  out.push(...deps.length ? deps : Object.keys(j.devDependencies ?? {}));
3072
3963
  } catch {
@@ -3176,9 +4067,9 @@ async function draftRepoType(repo, id) {
3176
4067
  fingerprint.push({ kind: "build-file", globs: buildGlobs, weight: 2, required: true });
3177
4068
  const deps = /* @__PURE__ */ new Map();
3178
4069
  for (const f of builds) {
3179
- const text = (await files.linesOf(f) ?? []).join("\n");
4070
+ const text2 = (await files.linesOf(f) ?? []).join("\n");
3180
4071
  const glob = buildGlobs.find((g) => globToRegExp(g).test(f));
3181
- for (const d of declaredDependencies(f, text)) {
4072
+ for (const d of declaredDependencies(f, text2)) {
3182
4073
  if (!PACKAGE_NAME.test(d) || d === repo.name)
3183
4074
  continue;
3184
4075
  if (!deps.has(d))
@@ -3740,7 +4631,7 @@ function parseEntry(s) {
3740
4631
  const m = /^([A-Za-z]+)\s+(\S+)$/.exec(s.trim());
3741
4632
  return m ? { method: m[1].toUpperCase(), path: m[2] } : { path: s.trim() };
3742
4633
  }
3743
- var segs = (p) => normalizePath(p).toLowerCase().split("/").filter(Boolean);
4634
+ var segs2 = (p) => normalizePath(p).toLowerCase().split("/").filter(Boolean);
3744
4635
  var sameMethod = (a, b) => !a || !b || a.toUpperCase() === b.toUpperCase();
3745
4636
  async function joinEstate(o) {
3746
4637
  const byId = new Map(o.records.map((r) => [r.id, r]));
@@ -3752,8 +4643,8 @@ async function joinEstate(o) {
3752
4643
  const out = [i.normalizedPath];
3753
4644
  const route = i.route ? byId.get(i.route) : void 0;
3754
4645
  if (route?.kind === "GatewayRoute") {
3755
- const base2 = segs(route.basepath);
3756
- const p = segs(i.normalizedPath);
4646
+ const base2 = segs2(route.basepath);
4647
+ const p = segs2(i.normalizedPath);
3757
4648
  if (base2.length && base2.every((s, k) => p[k] === s))
3758
4649
  out.push(`/${p.slice(base2.length).join("/")}`);
3759
4650
  }
@@ -3868,27 +4759,27 @@ async function joinEstate(o) {
3868
4759
  within = "estate";
3869
4760
  hits = servingExactly(flow.component, want);
3870
4761
  }
3871
- const candidates = [...new Set(hits.map((e) => e.component))].sort();
3872
- if (candidates.length === 0) {
4762
+ const candidates2 = [...new Set(hits.map((e) => e.component))].sort();
4763
+ if (candidates2.length === 0) {
3873
4764
  miss({
3874
4765
  reason: "no-target",
3875
4766
  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
4767
  });
3877
4768
  continue;
3878
4769
  }
3879
- if (candidates.length > 1) {
3880
- const repos = [...new Set(candidates.map(repoOfComponent))];
4770
+ if (candidates2.length > 1) {
4771
+ const repos = [...new Set(candidates2.map(repoOfComponent))];
3881
4772
  const of = repos.length === 1 && repos[0] ? repos[0] : "the estate";
3882
4773
  miss({
3883
4774
  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`
4775
+ 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
4776
  });
3886
4777
  asks.push({
3887
4778
  from,
3888
4779
  caller: flow.component,
3889
4780
  named: s.to.component,
3890
4781
  what: s.to.entry,
3891
- candidates
4782
+ candidates: candidates2
3892
4783
  });
3893
4784
  continue;
3894
4785
  }
@@ -4116,6 +5007,17 @@ function fieldLineage(f, field) {
4116
5007
  }
4117
5008
 
4118
5009
  // packages/scanner/dist/hosts-asked.js
5010
+ var segs3 = (p) => p.split(/[?#]/)[0].split("/").filter(Boolean).map((x) => /^[{:<$]/.test(x) ? "{}" : x);
5011
+ var underPath = (path, base2) => {
5012
+ const b = segs3(base2);
5013
+ const c = segs3(path);
5014
+ return b.length > 0 && b.length <= c.length && b.every((x, i) => x === c[i]);
5015
+ };
5016
+ var servesPath = (entry, path) => {
5017
+ const e = segs3(entry);
5018
+ const c = segs3(path);
5019
+ return e.length === c.length && e.every((x, i) => x === "{}" || c[i] === "{}" || x === c[i]);
5020
+ };
4119
5021
  var HTTP_TYPES = /* @__PURE__ */ new Set(["rest", "graphql", "soap", "websocket", "sse", "webhook"]);
4120
5022
  var CONFIG_FILES = [
4121
5023
  "**/application*.properties",
@@ -4178,8 +5080,12 @@ async function unknownHosts(o) {
4178
5080
  const entry = byHost.get(h.label) ?? {
4179
5081
  ...h.port !== void 0 ? { port: h.port } : {},
4180
5082
  explicit: h.label !== h.host,
4181
- calls: []
5083
+ calls: [],
5084
+ paths: /* @__PURE__ */ new Set()
4182
5085
  };
5086
+ const path = /^[a-z][a-z0-9+.-]*:\/\/[^/?#]*(\/[^?#]*)?/i.exec(url)?.[1];
5087
+ if (path)
5088
+ entry.paths.add(path);
4183
5089
  const at3 = `${e.path}:${e.line}`;
4184
5090
  if (!entry.calls.some((c) => c.repo === repo && c.at === at3))
4185
5091
  entry.calls.push({ repo, at: at3 });
@@ -4232,6 +5138,25 @@ async function unknownHosts(o) {
4232
5138
  }
4233
5139
  const suggest = hits.length === 1 ? hits[0] : void 0;
4234
5140
  const calls = entry.calls.sort((a, b) => a.repo.localeCompare(b.repo) || a.at.localeCompare(b.at, "en", { numeric: true }));
5141
+ const byPath = [];
5142
+ for (const g of o.records) {
5143
+ if (g.kind !== "GatewayRoute")
5144
+ continue;
5145
+ const repo = g.evidence.find((e) => e.repo)?.repo;
5146
+ if (!repo || byPath.some((b) => b.repo === repo))
5147
+ continue;
5148
+ const under2 = [...entry.paths].filter((p) => underPath(p, g.basepath));
5149
+ if (!under2.length)
5150
+ continue;
5151
+ const comps = new Set([...compRepo].filter(([, r]) => r === repo).map(([c]) => c));
5152
+ const hit = o.records.find((r) => r.kind === "Endpoint" && comps.has(r.component) && under2.some((p) => servesPath(r.pathOrTopic, p)));
5153
+ byPath.push({
5154
+ repo,
5155
+ serves: hit ? [hit.method?.toUpperCase(), hit.pathOrTopic].filter(Boolean).join(" ") : `the routes under ${g.basepath}`
5156
+ });
5157
+ }
5158
+ byPath.sort((a, b) => a.repo.localeCompare(b.repo));
5159
+ const first = suggest ? [suggest.repo] : byPath.map((b) => b.repo).filter((x) => servers.includes(x));
4235
5160
  const about = Object.fromEntries(servers.flatMap((s) => {
4236
5161
  const d = o.repos.find((r) => r.id === s)?.description;
4237
5162
  return d ? [[s, { text: d.text, trust: d.trust }]] : [];
@@ -4239,9 +5164,10 @@ async function unknownHosts(o) {
4239
5164
  asks.push({
4240
5165
  host,
4241
5166
  calls,
4242
- servers: suggest ? [suggest.repo, ...servers.filter((s) => s !== suggest.repo)] : servers,
5167
+ servers: [...first, ...servers.filter((s) => !first.includes(s))],
4243
5168
  ...suggest ? { suggest } : {},
4244
- ...Object.keys(about).length ? { about } : {}
5169
+ ...Object.keys(about).length ? { about } : {},
5170
+ ...byPath.length ? { byPath } : {}
4245
5171
  });
4246
5172
  }
4247
5173
  return asks;
@@ -4252,23 +5178,38 @@ function aboutOf(a, repo) {
4252
5178
  return "";
4253
5179
  return d.trust === "suggested" ? `. What it is, as your assistant drafted it (unconfirmed): ${d.text}` : `. What it is: ${d.text}`;
4254
5180
  }
5181
+ function gatewayHostFix(host, byPath) {
5182
+ const repos = byPath.map((b) => b.repo);
5183
+ 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`;
5184
+ }
4255
5185
  function hostQuestionDrafts(asks) {
4256
5186
  return asks.map((a) => {
4257
5187
  const repo = a.calls[0].repo;
5188
+ const byPath = new Map((a.byPath ?? []).map((b) => [b.repo, b.serves]));
4258
5189
  return {
4259
5190
  repo,
5191
+ ...a.byPath?.length ? { byPath: a.byPath } : {},
4260
5192
  draft: {
4261
5193
  question: hostQuestionText(a.host),
4262
5194
  options: [
4263
5195
  ...a.servers.map((s) => ({
4264
5196
  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`
5197
+ 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),
5198
+ 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`
5199
+ })),
5200
+ // O1: a system outside the estate, declared in the profile with the host as its alias.
5201
+ ...[
5202
+ [HOSTS_OUTSIDE_INTERNAL, "internal", "another team's system"],
5203
+ [HOSTS_OUTSIDE_VENDOR, "vendor", "a vendor's system"]
5204
+ ].map(([label2, boundary, what2]) => ({
5205
+ label: label2,
5206
+ means: `${a.host} is ${what2}, outside this estate, that the org pack doesn't name yet`,
5207
+ 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
5208
  })),
4268
5209
  {
4269
5210
  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"
5211
+ means: `Neither a repo of this estate nor a system to add to the org pack answers on ${a.host}`,
5212
+ changes: "Nothing is added to the manifest or the profile; the calls stay loose ends, and this isn't asked again"
4272
5213
  }
4273
5214
  ],
4274
5215
  evidence: a.calls.filter((c) => c.repo === repo).slice(0, 3).map((c) => c.at),
@@ -4278,6 +5219,375 @@ function hostQuestionDrafts(asks) {
4278
5219
  });
4279
5220
  }
4280
5221
 
5222
+ // packages/scanner/dist/measure-links.js
5223
+ var import_yaml4 = __toESM(require_dist(), 1);
5224
+ var METHODS = /^(GET|POST|PUT|PATCH|DELETE|HEAD|OPTIONS|TRACE|ANY)\s+(\S.*)$/i;
5225
+ var NEAR = 3;
5226
+ var AT = /^.+:\d+$/;
5227
+ var AT_HELP = "at is file:line, e.g. src/main/java/Client.java:16";
5228
+ var isObj2 = (x) => typeof x === "object" && x !== null && !Array.isArray(x);
5229
+ var text = (x) => typeof x === "string" && x.trim() ? x.trim() : typeof x === "number" ? String(x) : void 0;
5230
+ function parseLinkReference(source) {
5231
+ let raw;
5232
+ try {
5233
+ raw = (0, import_yaml4.parse)(source);
5234
+ } catch (e) {
5235
+ return { ok: false, issues: [`the file isn't YAML: ${e.message}`] };
5236
+ }
5237
+ const issues = [];
5238
+ const say = (where, message) => issues.push(`${where}: ${message}`);
5239
+ const only = (where, o, keys) => {
5240
+ for (const k of Object.keys(o))
5241
+ if (!keys.includes(k))
5242
+ say(where, `unknown key ${k}`);
5243
+ };
5244
+ if (!isObj2(raw) || !Array.isArray(raw.repos))
5245
+ return { ok: false, issues: ["repos: list the repos, each with its entries and calls"] };
5246
+ only("(top)", raw, ["estate", "repos"]);
5247
+ const repos = [];
5248
+ raw.repos.forEach((r, i) => {
5249
+ const w = `repos.${i}`;
5250
+ if (!isObj2(r) || !text(r.repo))
5251
+ return say(w, "each repo needs repo: <its id>");
5252
+ only(w, r, ["repo", "entries", "calls"]);
5253
+ const entries = [];
5254
+ const calls = [];
5255
+ const list = (x, name) => {
5256
+ if (x === void 0 || x === null)
5257
+ return [];
5258
+ if (Array.isArray(x))
5259
+ return x;
5260
+ say(`${w}.${name}`, `${name} is a list`);
5261
+ return [];
5262
+ };
5263
+ list(r.entries, "entries").forEach((e, j) => {
5264
+ const we = `${w}.entries.${j}`;
5265
+ if (text(e))
5266
+ return entries.push({ entry: text(e) });
5267
+ if (!isObj2(e))
5268
+ return say(we, "an entry is METHOD /path, or { entry, at, fact }");
5269
+ only(we, e, ["entry", "at", "fact"]);
5270
+ const at3 = text(e.at);
5271
+ if (at3 && !AT.test(at3))
5272
+ return say(`${we}.at`, AT_HELP);
5273
+ if (!text(e.entry) && !at3 && !text(e.fact))
5274
+ return say(we, "give the entry (METHOD /path), its line (at: file:line) or its fact id");
5275
+ entries.push(stripped({ entry: text(e.entry), at: at3, fact: text(e.fact) }));
5276
+ });
5277
+ list(r.calls, "calls").forEach((c, j) => {
5278
+ const wc = `${w}.calls.${j}`;
5279
+ if (!isObj2(c))
5280
+ return say(wc, "a call is { call, at, reaches } or { call, at, outside }");
5281
+ only(wc, c, ["call", "at", "fact", "reaches", "outside", "note"]);
5282
+ const reaches = text(c.reaches);
5283
+ const outside = text(c.outside);
5284
+ const at3 = text(c.at);
5285
+ let bad = false;
5286
+ if (reaches && outside) {
5287
+ say(wc, "give one of reaches (a repo, or none) and outside (a system), not both");
5288
+ bad = true;
5289
+ } else if (!reaches && !outside) {
5290
+ say(wc, "give one of reaches (a repo, or none) and outside (a system)");
5291
+ bad = true;
5292
+ }
5293
+ if (!text(c.call) && !at3 && !text(c.fact)) {
5294
+ say(wc, "give the call (METHOD /path), its line (at: file:line) or its fact id");
5295
+ bad = true;
5296
+ }
5297
+ if (at3 && !AT.test(at3)) {
5298
+ say(`${wc}.at`, AT_HELP);
5299
+ bad = true;
5300
+ }
5301
+ if (bad)
5302
+ return;
5303
+ const want = outside ? { outside } : reaches.toLowerCase() === "none" ? { none: true } : { repo: reaches };
5304
+ calls.push({
5305
+ ...stripped({ call: text(c.call), at: at3, fact: text(c.fact) }),
5306
+ want
5307
+ });
5308
+ });
5309
+ repos.push({ repo: text(r.repo), entries, calls });
5310
+ });
5311
+ if (issues.length)
5312
+ return { ok: false, issues };
5313
+ const estate = text(raw.estate);
5314
+ return { ok: true, reference: { ...estate ? { estate } : {}, repos } };
5315
+ }
5316
+ var stripped = (o) => Object.fromEntries(Object.entries(o).filter(([, v]) => v !== void 0));
5317
+ function segs4(path) {
5318
+ return path.split(/[?#]/)[0].split("/").filter(Boolean).map((s) => /^(\{[^}]*\}|:\w+|\*)$/.test(s) ? "{}" : s.toLowerCase());
5319
+ }
5320
+ function pathMatch(a, b) {
5321
+ const x = segs4(a);
5322
+ const y = segs4(b);
5323
+ if (x.length === y.length && x.every((s, i) => s === y[i]))
5324
+ return 2;
5325
+ const [short, long] = x.length < y.length ? [x, y] : [y, x];
5326
+ if (!short.length)
5327
+ return 0;
5328
+ const off = long.length - short.length;
5329
+ return short.every((s, i) => s === long[off + i]) ? 1 : 0;
5330
+ }
5331
+ function parseWhat(what2) {
5332
+ if (!what2)
5333
+ return {};
5334
+ const m = METHODS.exec(what2.trim());
5335
+ return m ? { method: m[1].toUpperCase(), path: m[2].trim() } : { path: what2.trim() };
5336
+ }
5337
+ function parseAt(at3) {
5338
+ if (!at3)
5339
+ return void 0;
5340
+ const i = at3.lastIndexOf(":");
5341
+ return { file: at3.slice(0, i).replace(/^\.\//, ""), line: Number(at3.slice(i + 1)) };
5342
+ }
5343
+ function finder(facts) {
5344
+ const used = /* @__PURE__ */ new Set();
5345
+ return (ref) => {
5346
+ const { method, path } = parseWhat(ref.what);
5347
+ const at3 = parseAt(ref.at);
5348
+ let best;
5349
+ for (const f of facts) {
5350
+ if (used.has(f.id))
5351
+ continue;
5352
+ if (ref.fact) {
5353
+ if (f.id === ref.fact)
5354
+ best = { f, score: 1e6 };
5355
+ continue;
5356
+ }
5357
+ let score = 0;
5358
+ if (at3) {
5359
+ const d = Math.abs(f.cite.line - at3.line);
5360
+ if (f.cite.file !== at3.file || d > NEAR)
5361
+ continue;
5362
+ score += 10 * (NEAR + 1 - d);
5363
+ }
5364
+ if (method && f.method && f.method.toUpperCase() !== method && method !== "ANY")
5365
+ continue;
5366
+ if (path) {
5367
+ const p = pathMatch(path, f.path);
5368
+ if (!p)
5369
+ continue;
5370
+ score += 100 * p;
5371
+ }
5372
+ if (!best || score > best.score)
5373
+ best = { f, score };
5374
+ }
5375
+ if (best)
5376
+ used.add(best.f.id);
5377
+ return best?.f;
5378
+ };
5379
+ }
5380
+ var round = (n, d) => d ? Math.round(n / d * 1e3) / 1e3 : null;
5381
+ var what = (f) => [f.method?.toUpperCase(), f.path].filter(Boolean).join(" ");
5382
+ function measureLinks(input) {
5383
+ const { op } = input;
5384
+ const repos = input.repos.filter((x) => !isPrototypeRepo(op, x.repo));
5385
+ const renames = componentRenames(repos, op);
5386
+ const join10 = joinFacts({
5387
+ repos: joinReposOf(op, repos, renames),
5388
+ ...input.targets ? { targets: input.targets } : {},
5389
+ outside: outsideSystems(op),
5390
+ ...joinConventions(op),
5391
+ walkGateways: input.walkGateways ?? helperOn("gateway-walk")
5392
+ });
5393
+ const key = (repo, fact) => `${repo}\0${fact}`;
5394
+ const joins = /* @__PURE__ */ new Map();
5395
+ for (const j of join10.joins) {
5396
+ const k = key(j.call.repo, j.call.fact);
5397
+ joins.set(k, [...joins.get(k) ?? [], j]);
5398
+ }
5399
+ const asks = new Map(join10.asks.map((a) => [key(a.call.repo, a.call.fact), a]));
5400
+ const misses = new Map(join10.misses.map((m) => [key(m.call.repo, m.call.fact), m]));
5401
+ const outsideOf = new Map((join10.outside ?? []).map((o) => [key(o.call.repo, o.call.fact), o]));
5402
+ const own = new Set(estateRepos(op).flatMap((r) => r.system ? [r.system] : []));
5403
+ const systemOf = /* @__PURE__ */ new Map();
5404
+ for (const s of op.profiles.flatMap((p) => p.profile?.systems ?? []))
5405
+ if (s.boundary === "vendor" || !own.has(s.id))
5406
+ for (const n of [s.id, s.name, ...s.aliases])
5407
+ systemOf.set(factIdPart(n), s.id);
5408
+ const outsideName = (n) => n ? systemOf.get(factIdPart(n)) : void 0;
5409
+ const manifests = estateRepos(op);
5410
+ const repoOf = (name) => {
5411
+ const slug = factIdPart(name);
5412
+ const m = manifests.find((r) => r.id === name || factIdPart(r.id) === slug) ?? manifests.find((r) => r.component && factIdPart(r.component) === slug);
5413
+ return m?.id ?? name;
5414
+ };
5415
+ const factsOf = new Map(repos.map((r) => [r.repo, r.facts]));
5416
+ const notes = [];
5417
+ const reading = {
5418
+ entries: { want: 0, found: 0 },
5419
+ calls: { want: 0, found: 0 },
5420
+ missed: []
5421
+ };
5422
+ const rows = [];
5423
+ for (const r of input.reference.repos) {
5424
+ const facts = factsOf.get(r.repo);
5425
+ if (!facts)
5426
+ 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}).`);
5427
+ const all = facts ?? [];
5428
+ const findEntry = finder(all.filter((f) => f.kind === "entry"));
5429
+ for (const e of r.entries) {
5430
+ reading.entries.want++;
5431
+ if (findEntry({ what: e.entry, at: e.at, fact: e.fact }))
5432
+ reading.entries.found++;
5433
+ else
5434
+ reading.missed.push(`${r.repo} entry ${e.entry ?? e.fact ?? e.at}`);
5435
+ }
5436
+ const findCall = finder(all.filter((f) => f.kind === "call"));
5437
+ for (const c of r.calls) {
5438
+ reading.calls.want++;
5439
+ const f = findCall({ what: c.call, at: c.at, fact: c.fact });
5440
+ const label2 = c.call ?? (f ? what(f) : c.fact ?? c.at);
5441
+ const base2 = { repo: r.repo, call: label2, ...c.at ? { at: c.at } : {} };
5442
+ if (!f) {
5443
+ reading.missed.push(`${r.repo} call ${label2}`);
5444
+ rows.push({ ...base2, want: c.want, got: { kind: "not-read" }, right: false });
5445
+ continue;
5446
+ }
5447
+ reading.calls.found++;
5448
+ const got = gotOf(r.repo, f, all);
5449
+ rows.push({ ...base2, fact: f.id, want: c.want, got, right: isRight(c.want, got) });
5450
+ }
5451
+ }
5452
+ function gotOf(repo, f, facts) {
5453
+ const k = key(repo, f.id);
5454
+ const j = joins.get(k)?.[0];
5455
+ if (j) {
5456
+ const via = (j.gateway ?? []).map((h) => h.entry.repo);
5457
+ const inferred2 = joinGuessesOf(j)[0];
5458
+ return {
5459
+ kind: "joined",
5460
+ repo: j.entry.repo,
5461
+ component: j.entry.component,
5462
+ entry: [j.method, j.path].filter(Boolean).join(" "),
5463
+ ...via.length ? { via } : {},
5464
+ ...inferred2 ? { inferred: inferred2 } : {}
5465
+ };
5466
+ }
5467
+ const ask = asks.get(k);
5468
+ if (ask)
5469
+ return { kind: "asked", reason: ask.reason };
5470
+ const miss = misses.get(k);
5471
+ if (miss)
5472
+ return { kind: "missed", target: miss.target };
5473
+ const answer = input.targets?.[repo]?.[f.id];
5474
+ if (answer === NO_TARGET)
5475
+ return { kind: "unlinked" };
5476
+ const host = callHostOf(f, facts);
5477
+ const system = outsideName(answer) ?? outsideName(outsideOf.get(k)?.target) ?? outsideName(f.to) ?? outsideName(host);
5478
+ if (system)
5479
+ return { kind: "outside", system };
5480
+ if (f.to && host)
5481
+ return { kind: "outside", system: f.to };
5482
+ return { kind: "unlinked" };
5483
+ }
5484
+ function isRight(want, got) {
5485
+ if ("repo" in want) {
5486
+ if (got.kind !== "joined")
5487
+ return false;
5488
+ const w = repoOf(want.repo);
5489
+ return got.repo === w || got.component === factIdPart(want.repo);
5490
+ }
5491
+ if ("outside" in want) {
5492
+ if (got.kind !== "outside")
5493
+ return false;
5494
+ const w = outsideName(want.outside) ?? factIdPart(want.outside);
5495
+ return got.system === w || factIdPart(got.system) === factIdPart(want.outside);
5496
+ }
5497
+ return got.kind === "unlinked" || got.kind === "missed";
5498
+ }
5499
+ const toRepo = rows.filter((x) => "repo" in x.want);
5500
+ const joined = rows.filter((x) => x.got.kind === "joined");
5501
+ const right = joined.filter((x) => x.right).length;
5502
+ const inferred = joined.filter((x) => x.got.kind === "joined" && x.got.inferred);
5503
+ const outsideWant = rows.filter((x) => "outside" in x.want);
5504
+ const noneWant = rows.filter((x) => "none" in x.want);
5505
+ return {
5506
+ ...input.reference.estate ? { estate: input.reference.estate } : {},
5507
+ reading,
5508
+ links: {
5509
+ want: toRepo.length,
5510
+ joined: joined.length,
5511
+ right,
5512
+ precision: round(right, joined.length),
5513
+ recall: round(toRepo.filter((x) => x.right).length, toRepo.length),
5514
+ asked: rows.filter((x) => x.got.kind === "asked").length,
5515
+ missed: rows.filter((x) => x.got.kind === "missed").length,
5516
+ unlinked: rows.filter((x) => x.got.kind === "unlinked").length,
5517
+ notRead: rows.filter((x) => x.got.kind === "not-read").length,
5518
+ inferred: { joined: inferred.length, right: inferred.filter((x) => x.right).length },
5519
+ outside: {
5520
+ want: outsideWant.length,
5521
+ got: rows.filter((x) => x.got.kind === "outside").length,
5522
+ right: outsideWant.filter((x) => x.right).length
5523
+ },
5524
+ none: { want: noneWant.length, right: noneWant.filter((x) => x.right).length }
5525
+ },
5526
+ calls: rows,
5527
+ notes
5528
+ };
5529
+ }
5530
+
5531
+ // packages/scanner/dist/audit-links.js
5532
+ function estateJoinOf(op, list, targets, pairs, aiTargets) {
5533
+ const repos = list.filter((x) => !isPrototypeRepo(op, x.repo));
5534
+ const renames = componentRenames(repos, op);
5535
+ return joinFacts({
5536
+ repos: joinReposOf(op, repos, renames),
5537
+ ...targets ? { targets } : {},
5538
+ ...aiTargets && Object.keys(aiTargets).length ? { aiTargets } : {},
5539
+ ...pairs && Object.keys(pairs).length ? { pairs } : {},
5540
+ outside: outsideSystems(op),
5541
+ // The org pack's facing words and gateway bundle folders (ADR 0147), as the catalog joins.
5542
+ ...joinConventions(op),
5543
+ walkGateways: helperOn("gateway-walk")
5544
+ });
5545
+ }
5546
+ function auditLinkCandidates(join10, list, repo) {
5547
+ const facts = /* @__PURE__ */ new Map();
5548
+ for (const r of list)
5549
+ for (const f of r.facts)
5550
+ facts.set(`${r.repo}\0${f.id}`, f);
5551
+ const mine = join10.joins.filter((j) => j.call.repo === repo);
5552
+ const perCall = /* @__PURE__ */ new Map();
5553
+ for (const j of mine)
5554
+ perCall.set(j.call.fact, (perCall.get(j.call.fact) ?? 0) + 1);
5555
+ const at3 = (e) => `${e.repo} ${e.at}`;
5556
+ const out = [];
5557
+ for (const j of mine) {
5558
+ const call = facts.get(`${j.call.repo}\0${j.call.fact}`);
5559
+ const said = [call?.method?.toUpperCase(), call?.path].filter(Boolean).join(" ");
5560
+ const via = (j.gateway ?? []).map((h) => h.entry.repo);
5561
+ const flags = joinFlagsOf(j);
5562
+ const cites = [
5563
+ at3(j.call),
5564
+ ...j.tie ? [at3(j.tie)] : [],
5565
+ ...(j.through ?? []).map(at3),
5566
+ ...(j.exposedBy ?? []).map(at3),
5567
+ ...(j.gateway ?? []).flatMap((h) => [
5568
+ at3(h.entry),
5569
+ at3(h.call),
5570
+ ...h.tie ? [at3(h.tie)] : [],
5571
+ ...(h.through ?? []).map(at3),
5572
+ ...(h.exposedBy ?? []).map(at3)
5573
+ ]),
5574
+ ...(j.entryThrough ?? []).map(at3),
5575
+ at3(j.entry)
5576
+ ];
5577
+ out.push({
5578
+ id: `link:${j.call.fact}${(perCall.get(j.call.fact) ?? 0) > 1 ? `~${j.entry.fact}` : ""}`,
5579
+ kind: "link",
5580
+ // A publish joined to its listener carries no flag: its topic says how (ADR 0135).
5581
+ of: flags[0] ?? (j.how === "topic" ? "topic" : `joined-by-${j.how}`),
5582
+ claim: `${repo} ${said || j.call.fact} -> ${j.entry.repo} ${[j.method, j.path].filter(Boolean).join(" ")}${via.length ? ` via ${via.join(", ")}` : ""}`,
5583
+ cites: [...new Set(cites)],
5584
+ fact: j.call.fact,
5585
+ to: j.entry.repo
5586
+ });
5587
+ }
5588
+ return out.sort((a, b) => a.id < b.id ? -1 : a.id > b.id ? 1 : 0);
5589
+ }
5590
+
4281
5591
  export {
4282
5592
  buildGlossary,
4283
5593
  GLOSSARY_CHANGE,
@@ -4300,31 +5610,99 @@ export {
4300
5610
  diffCatalogs,
4301
5611
  diffIsEmpty,
4302
5612
  estateRepos,
5613
+ productionRepos,
5614
+ prototypeScanNotes,
4303
5615
  isPrototypeRepo,
5616
+ estateMergeInput,
5617
+ mergeEstate,
5618
+ curatedDirOf,
5619
+ TARGETS_MAX_SUGGESTED,
5620
+ TARGETS_ANOTHER,
5621
+ TARGETS_NO_EVIDENCE,
5622
+ nameWords,
5623
+ targetSuggestions,
5624
+ offeredSuggestions,
5625
+ targetPicker,
5626
+ pickerLines,
5627
+ topicQuestionDrafts,
4304
5628
  TARGETS_STEP,
5629
+ TARGETS_NONE,
5630
+ remapIds,
5631
+ targetQuestionFor,
5632
+ suggestionWhy,
4305
5633
  targetQuestionDrafts,
5634
+ TARGETS_SPLIT,
5635
+ targetPathPrefix,
5636
+ targetGroupQuestion,
5637
+ targetGroupWordings,
5638
+ splitTargetGroups,
5639
+ groupedTargetDrafts,
5640
+ targetRuleOffers,
5641
+ ruleCoversCall,
5642
+ ruledTargets,
4306
5643
  targetsFromAnswers,
5644
+ answeredQuestionOfCall,
5645
+ answerReconfirmed,
5646
+ staleTargetQuestions,
5647
+ componentRenames,
5648
+ factsRecords,
4307
5649
  outsideSystems,
5650
+ outsideProfileSystems,
5651
+ hostAliases,
4308
5652
  joinReposOf,
5653
+ applyFactJoins,
4309
5654
  catalogWithFacts,
4310
5655
  prototypeLeftOut,
5656
+ estateFromFacts,
5657
+ answerRuleOffers,
5658
+ factsForScan,
5659
+ pairsOfRepo,
4311
5660
  estateFacts,
5661
+ NO_SETTINGS,
4312
5662
  citedLine,
5663
+ isCodeFile,
5664
+ isConfigFile,
5665
+ globName,
5666
+ prefixOf,
4313
5667
  checkScanSettings,
5668
+ joinBase,
5669
+ applyClients,
5670
+ withExcludes,
5671
+ applyPrefixes,
5672
+ pluginExtractor,
4314
5673
  runPluginTests,
5674
+ pluginApplies,
5675
+ usablePlugins,
5676
+ withPluginScan,
5677
+ orgPackDirFor,
4315
5678
  scanEstate,
5679
+ CLEAR_MATCH,
5680
+ PARTIAL_MATCH,
5681
+ TIE_MARGIN,
4316
5682
  matchRepoType,
4317
5683
  resolveSettingsTemplate,
4318
5684
  draftRepoType,
4319
5685
  repoComponents,
5686
+ pathScore,
5687
+ resourceKey,
5688
+ familyMismatches,
4320
5689
  specTriggers,
5690
+ lineNamesUiWord,
5691
+ uiFindings,
4321
5692
  checkSpec,
4322
5693
  staleFlows,
4323
5694
  citedFileHashes,
4324
5695
  estateStepKey,
4325
5696
  joinEstate,
4326
5697
  estateFlow,
5698
+ flattenEstateFlow,
4327
5699
  fieldLineage,
5700
+ lineSetsPort,
4328
5701
  unknownHosts,
4329
- hostQuestionDrafts
5702
+ gatewayHostFix,
5703
+ hostQuestionDrafts,
5704
+ parseLinkReference,
5705
+ measureLinks,
5706
+ estateJoinOf,
5707
+ auditLinkCandidates
4330
5708
  };