project-athena 0.2.1 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (147) hide show
  1. package/CHANGELOG.md +38 -0
  2. package/README.md +15 -8
  3. package/dist/activity-P4UUFHWV.js +77 -0
  4. package/dist/agents-3HTSQGL2.js +69 -0
  5. package/dist/ai-ASL4FFQ7.js +96 -0
  6. package/dist/analyze-F3YOILNL.js +49 -0
  7. package/dist/chunk-2DY4PUR4.js +465 -0
  8. package/dist/chunk-2R6PXY6Z.js +33 -0
  9. package/dist/chunk-2RS6TA3D.js +908 -0
  10. package/dist/chunk-32PRS2MG.js +169 -0
  11. package/dist/chunk-3IS2X2Z7.js +89 -0
  12. package/dist/chunk-65TSRDQ3.js +124 -0
  13. package/dist/chunk-673EPYYH.js +105 -0
  14. package/dist/chunk-AD4OZCU2.js +34 -0
  15. package/dist/chunk-CXUTYCZX.js +238 -0
  16. package/dist/chunk-DOQMAZL5.js +436 -0
  17. package/dist/chunk-EF6GHOIN.js +343 -0
  18. package/dist/chunk-ELKFRTZT.js +221 -0
  19. package/dist/chunk-EUPETUWA.js +14 -0
  20. package/dist/chunk-FQEGEVLY.js +38 -0
  21. package/dist/chunk-GHFRQAYT.js +112 -0
  22. package/dist/chunk-GR4DL2VM.js +47 -0
  23. package/dist/chunk-INT3IGMQ.js +111 -0
  24. package/dist/chunk-IQQBAWSM.js +90 -0
  25. package/dist/chunk-JCGDR4UV.js +182 -0
  26. package/dist/chunk-JY6BFIJJ.js +95 -0
  27. package/dist/chunk-KBO7XRBW.js +26 -0
  28. package/dist/chunk-LEMPCNAY.js +209 -0
  29. package/dist/chunk-LMOFZNTQ.js +379 -0
  30. package/dist/chunk-OLPSEQS5.js +264 -0
  31. package/dist/chunk-PANVPNYF.js +2892 -0
  32. package/dist/chunk-PDFQ5BFG.js +378 -0
  33. package/dist/chunk-PR53DT7I.js +179 -0
  34. package/dist/chunk-PVCIGUIT.js +64 -0
  35. package/dist/chunk-RXNRMIZP.js +390 -0
  36. package/dist/chunk-S7T6VX56.js +61 -0
  37. package/dist/chunk-SHKXELUX.js +295 -0
  38. package/dist/chunk-SJOOXO34.js +904 -0
  39. package/dist/chunk-TGJUUB44.js +399 -0
  40. package/dist/chunk-VSYBC3VY.js +28 -0
  41. package/dist/chunk-XB66PZ6Z.js +182 -0
  42. package/dist/chunk-XFN7UZCS.js +35 -0
  43. package/dist/chunk-XW3MLHI2.js +32 -0
  44. package/dist/chunk-Z6QWMJLK.js +124 -0
  45. package/dist/chunk-ZB7LC6FP.js +221 -0
  46. package/dist/clean-S7PKCOLM.js +23 -0
  47. package/dist/cli.js +18 -10716
  48. package/dist/context-AH5MFHRG.js +147 -0
  49. package/dist/doctor-RSJMLE2P.js +62 -0
  50. package/dist/event-TNT6CQZR.js +101 -0
  51. package/dist/git-hook-YZNZGAAP.js +252 -0
  52. package/dist/init-2V34DVRQ.js +37 -0
  53. package/dist/mcp-T2GRTEX5.js +255 -0
  54. package/dist/open-2QSZKKUG.js +1259 -0
  55. package/dist/program-V2USHGXN.js +148 -0
  56. package/dist/review-R3R2J5BJ.js +115 -0
  57. package/dist/rules-4FXJUCXB.js +83 -0
  58. package/dist/security-PI4LXCOB.js +175 -0
  59. package/dist/staged-check-HHIBKKMR.js +500 -0
  60. package/dist/status-6M73WX5I.js +75 -0
  61. package/dist/sync-3ZNJDSSU.js +216 -0
  62. package/dist/watch-USN5CQIV.js +121 -0
  63. package/dist/web/assets/{Editor-DAXvniWB.js → Editor-BI56-HX5.js} +1 -1
  64. package/dist/web/assets/{abnfDiagram-O67JEVCF-Az7EW1BC.js → abnfDiagram-O67JEVCF-y_9px2ob.js} +1 -1
  65. package/dist/web/assets/architecture-7GRP2DOG-DyxkwYiW.js +1 -0
  66. package/dist/web/assets/{architectureDiagram-NJMV4G6O-BxeXqSTn.js → architectureDiagram-NJMV4G6O-lB6fcVzU.js} +1 -1
  67. package/dist/web/assets/{blockDiagram-BEXU5L5S-aeKZl9lJ.js → blockDiagram-BEXU5L5S-DcM5pEjM.js} +1 -1
  68. package/dist/web/assets/{c4Diagram-YGBWAQC7-CgmntYsG.js → c4Diagram-YGBWAQC7-CJgqxTY2.js} +1 -1
  69. package/dist/web/assets/channel-BvDD7AMO.js +1 -0
  70. package/dist/web/assets/{chunk-3FUC2YCW-BpmRD2yo.js → chunk-3FUC2YCW-B8l_VuWR.js} +1 -1
  71. package/dist/web/assets/{chunk-5DYCD2WN-CRIFDwod.js → chunk-5DYCD2WN-CZecgNhf.js} +1 -1
  72. package/dist/web/assets/{chunk-742MDFTN-hRW9VeNV.js → chunk-742MDFTN-BeZ7P5jB.js} +1 -1
  73. package/dist/web/assets/{chunk-7INBJB4K-C6VEpT0Y.js → chunk-7INBJB4K-IKy0pr5m.js} +1 -1
  74. package/dist/web/assets/{chunk-7M6MHVWA-Dnajov2w.js → chunk-7M6MHVWA-Dg_QqJEX.js} +1 -1
  75. package/dist/web/assets/{chunk-7PRAP22T-DSAc0_CZ.js → chunk-7PRAP22T-DqQNS8oq.js} +1 -1
  76. package/dist/web/assets/{chunk-GTNCS2PH-CefpYUjb.js → chunk-GTNCS2PH-x4sM51nJ.js} +1 -1
  77. package/dist/web/assets/{chunk-GWA4HPMP-MLNflMmF.js → chunk-GWA4HPMP-CE5GcwVN.js} +1 -1
  78. package/dist/web/assets/{chunk-MBY4JIJT-D3GmmEiU.js → chunk-MBY4JIJT-DA_D1hjF.js} +1 -1
  79. package/dist/web/assets/{chunk-NETBCI7D-DzTj6xVk.js → chunk-NETBCI7D-CeJ3XBZ2.js} +1 -1
  80. package/dist/web/assets/{chunk-O7XYJQB3-D1d0hCko.js → chunk-O7XYJQB3-BH2gESvO.js} +1 -1
  81. package/dist/web/assets/{chunk-UA2S7LBM-C9hIFeWR.js → chunk-UA2S7LBM-BWr0xCux.js} +1 -1
  82. package/dist/web/assets/{chunk-WEXAMYUT-BFGtcjsi.js → chunk-WEXAMYUT-B4QwUZPM.js} +1 -1
  83. package/dist/web/assets/{chunk-Z7XXMR3K-BRZ8_wtE.js → chunk-Z7XXMR3K-BxcA-y5B.js} +1 -1
  84. package/dist/web/assets/{chunk-ZIGJFQKS-CTKOioal.js → chunk-ZIGJFQKS-BZYvP63G.js} +1 -1
  85. package/dist/web/assets/{classDiagram-v2-NBCMYWYE-DJGKtWJv.js → classDiagram-v2-NBCMYWYE-BS3P_NXb.js} +1 -1
  86. package/dist/web/assets/{cynefin-OW5HDTMX-hNq1kGmA.js → cynefin-OW5HDTMX-BALD4Pve.js} +1 -1
  87. package/dist/web/assets/{cynefinDiagram-VND7K2PF-sODQM_ZZ.js → cynefinDiagram-VND7K2PF-BM2y4W6d.js} +1 -1
  88. package/dist/web/assets/{dagre-6A5THRUB-DEzTThIf.js → dagre-6A5THRUB-PTfOrIdF.js} +1 -1
  89. package/dist/web/assets/{diagram-22UHCM2B-s_eellsa.js → diagram-22UHCM2B-CpXEHhUl.js} +1 -1
  90. package/dist/web/assets/{diagram-3UASUU5V-B2onY_Xw.js → diagram-3UASUU5V-DPHfXzr6.js} +1 -1
  91. package/dist/web/assets/{diagram-ATOU4E4O-B8V6s13s.js → diagram-ATOU4E4O-DMBNXvAC.js} +1 -1
  92. package/dist/web/assets/{diagram-CDSNMT55-CJxiE6mf.js → diagram-CDSNMT55-CDLe1M7G.js} +1 -1
  93. package/dist/web/assets/{diagram-MLGK6HIB-m5OHPLAp.js → diagram-MLGK6HIB-D6L8oE7j.js} +1 -1
  94. package/dist/web/assets/{diagram-MPIPVDR6-Buvvrk_A.js → diagram-MPIPVDR6-CP-S7rrQ.js} +1 -1
  95. package/dist/web/assets/{ebnfDiagram-ZINNZB2B-CpL7Wt3R.js → ebnfDiagram-ZINNZB2B-CkUc63TK.js} +1 -1
  96. package/dist/web/assets/{elk-276RUBZZ-CwYhbsXa.js → elk-276RUBZZ-C1CAJLH_.js} +1 -1
  97. package/dist/web/assets/{erDiagram-OPXOYQCR-DNikci2r.js → erDiagram-OPXOYQCR-DXBnaTHm.js} +1 -1
  98. package/dist/web/assets/eventmodeling-NTZA5JFV-cLIzvgDp.js +1 -0
  99. package/dist/web/assets/flowDiagram-KWPJA3E3-D7Zq63VQ.js +1 -0
  100. package/dist/web/assets/{ganttDiagram-FUAMR5RP-d-ynDlvJ.js → ganttDiagram-FUAMR5RP-BVm8SiGg.js} +1 -1
  101. package/dist/web/assets/{gitGraph-4MIJSDKK-DwIeBI91.js → gitGraph-4MIJSDKK-Dm7xBrW0.js} +1 -1
  102. package/dist/web/assets/{gitGraphDiagram-X574FWY7-04RZccu6.js → gitGraphDiagram-X574FWY7-D_PIKKhx.js} +1 -1
  103. package/dist/web/assets/{index-C3-8uYXr.js → index-CeOIBpu2.js} +3 -3
  104. package/dist/web/assets/{info-A6RAGUB7-xf4Nzgv-.js → info-A6RAGUB7-CLGrx8pn.js} +1 -1
  105. package/dist/web/assets/{infoDiagram-VRGFBTTK-BYjw0eiK.js → infoDiagram-VRGFBTTK-BlBjaKm7.js} +1 -1
  106. package/dist/web/assets/{ishikawaDiagram-OU5B5YK6-Bx9s4nPM.js → ishikawaDiagram-OU5B5YK6-Kxe7GGS3.js} +1 -1
  107. package/dist/web/assets/{journeyDiagram-ZHPQQLJL-DrdHs7B-.js → journeyDiagram-ZHPQQLJL-wCMoHK4M.js} +1 -1
  108. package/dist/web/assets/{kanban-definition-PNTS6WVX-CVd8v2Te.js → kanban-definition-PNTS6WVX-BxdCz9Ko.js} +1 -1
  109. package/dist/web/assets/{line-V_WrJ-6y.js → line-Zf11FnWF.js} +1 -1
  110. package/dist/web/assets/{mermaid-parser.core-B-ilyEpO.js → mermaid-parser.core-BvFFklev.js} +3 -3
  111. package/dist/web/assets/{mermaid.core-Bnyaa0nF.js → mermaid.core-Cd_Ssti9.js} +4 -4
  112. package/dist/web/assets/{mindmap-definition-NLK3R4M7-B3AbvHPR.js → mindmap-definition-NLK3R4M7-BGPQl-GY.js} +1 -1
  113. package/dist/web/assets/{packet-AYTQ26CC-CBAIUXIc.js → packet-AYTQ26CC-6lI3cEX_.js} +1 -1
  114. package/dist/web/assets/{pegDiagram-GJSIUBJH-DIdeELUM.js → pegDiagram-GJSIUBJH-BPIjlSAj.js} +1 -1
  115. package/dist/web/assets/{pie-WAS4IAKB-a1r3tzPR.js → pie-WAS4IAKB-D6Dzj3Ja.js} +1 -1
  116. package/dist/web/assets/{pieDiagram-5QR66LMP-pQBEio0x.js → pieDiagram-5QR66LMP-DtA-vZmv.js} +1 -1
  117. package/dist/web/assets/{quadrantDiagram-O4NWA36T-CkJK5Y03.js → quadrantDiagram-O4NWA36T-EettyqQn.js} +1 -1
  118. package/dist/web/assets/{radar-RG4KPBEZ-BZfLYjx5.js → radar-RG4KPBEZ-xmRc3Dl8.js} +1 -1
  119. package/dist/web/assets/{railroad-74A4TZTK-CuIW1ZAU.js → railroad-74A4TZTK-BLB7dhOn.js} +1 -1
  120. package/dist/web/assets/railroad-abnf-HS5TGJTU-DBFym6g8.js +1 -0
  121. package/dist/web/assets/railroad-ebnf-LZEXJU2U-CkG28ig8.js +1 -0
  122. package/dist/web/assets/railroad-peg-WCYAUIDC-Df_Ifaad.js +1 -0
  123. package/dist/web/assets/{railroadDiagram-XR7U4H2S-_BpGE8aJ.js → railroadDiagram-XR7U4H2S-PBw4We6b.js} +1 -1
  124. package/dist/web/assets/{requirementDiagram-PLB6GJNP-CnzF90GS.js → requirementDiagram-PLB6GJNP-DjQpaUFA.js} +1 -1
  125. package/dist/web/assets/{sankeyDiagram-IPEJSGJF-DYeisDj_.js → sankeyDiagram-IPEJSGJF-CBW-_AoF.js} +1 -1
  126. package/dist/web/assets/{sequenceDiagram-PO4LG4MO-ll9cBvca.js → sequenceDiagram-PO4LG4MO-DEoPYqY9.js} +1 -1
  127. package/dist/web/assets/{stateDiagram-v2-GCMORJYK-CdKcOIKQ.js → stateDiagram-v2-GCMORJYK-UZq5rPud.js} +1 -1
  128. package/dist/web/assets/{swimlanes-2SLR337P-y6piTWEW.js → swimlanes-2SLR337P-CACfuXJR.js} +1 -1
  129. package/dist/web/assets/swimlanesDiagram-TC7HE7FX-CDSrqdgx.js +8 -0
  130. package/dist/web/assets/{timeline-definition-EJHVYXUP-DuC_4xK0.js → timeline-definition-EJHVYXUP-CYJl7mP-.js} +1 -1
  131. package/dist/web/assets/{treeView-Q6P3EWNA-gCQFmND0.js → treeView-Q6P3EWNA-DtZ5nkPQ.js} +1 -1
  132. package/dist/web/assets/{treemap-WGGIJYW6-BQ2010OY.js → treemap-WGGIJYW6-m49GDM-M.js} +1 -1
  133. package/dist/web/assets/{usecaseDiagram-POWQR4AR-CCTqzHz6.js → usecaseDiagram-POWQR4AR-C2Vg20UK.js} +1 -1
  134. package/dist/web/assets/{vennDiagram-UO4OBE2U-0_O7guNq.js → vennDiagram-UO4OBE2U-C07v5OGI.js} +1 -1
  135. package/dist/web/assets/{wardley-WFR3VGLG-SsX7uMDs.js → wardley-WFR3VGLG-C9Z7EZtz.js} +1 -1
  136. package/dist/web/assets/{wardleyDiagram-VNRHLVJA-DlqH6Hox.js → wardleyDiagram-VNRHLVJA-DdA8CcSC.js} +1 -1
  137. package/dist/web/assets/{xychartDiagram-PMCCYNJV-nr4UYDtd.js → xychartDiagram-PMCCYNJV-C9EZw07b.js} +1 -1
  138. package/dist/web/index.html +1 -1
  139. package/package.json +4 -2
  140. package/dist/web/assets/architecture-7GRP2DOG-blXBarB2.js +0 -1
  141. package/dist/web/assets/channel-f4QzewnN.js +0 -1
  142. package/dist/web/assets/eventmodeling-NTZA5JFV-CA_ZFPZ2.js +0 -1
  143. package/dist/web/assets/flowDiagram-KWPJA3E3-DObsBuWT.js +0 -1
  144. package/dist/web/assets/railroad-abnf-HS5TGJTU-6B4Ffbxn.js +0 -1
  145. package/dist/web/assets/railroad-ebnf-LZEXJU2U-BGL9lIQc.js +0 -1
  146. package/dist/web/assets/railroad-peg-WCYAUIDC-DcqxTem7.js +0 -1
  147. package/dist/web/assets/swimlanesDiagram-TC7HE7FX-DFS8a3jO.js +0 -8
@@ -0,0 +1,2892 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ IgnoreMatcher,
4
+ SOURCE_FILE,
5
+ TEST_FILE,
6
+ factsConfigHash,
7
+ mapLimit,
8
+ sniffBinary,
9
+ walkProject
10
+ } from "./chunk-ELKFRTZT.js";
11
+ import {
12
+ getOrCreateSalt,
13
+ keyedFingerprint,
14
+ readLocalConfig
15
+ } from "./chunk-PVCIGUIT.js";
16
+ import {
17
+ gitInfo
18
+ } from "./chunk-OLPSEQS5.js";
19
+ import {
20
+ ephemeralFingerprint,
21
+ isSecretLikeName,
22
+ redact,
23
+ scanText
24
+ } from "./chunk-JCGDR4UV.js";
25
+ import {
26
+ FactsCache,
27
+ athenaDir,
28
+ loadConfig,
29
+ readFileIndex
30
+ } from "./chunk-2DY4PUR4.js";
31
+ import {
32
+ readTextIfExists,
33
+ writeFileAtomic
34
+ } from "./chunk-IQQBAWSM.js";
35
+ import {
36
+ baseName,
37
+ dirOf
38
+ } from "./chunk-XW3MLHI2.js";
39
+ import {
40
+ ATHENA_DIR
41
+ } from "./chunk-EUPETUWA.js";
42
+
43
+ // src/core/analyzer/detectors/env.ts
44
+ import { promises as fs } from "fs";
45
+ import path from "path";
46
+ var TEMPLATE_ENV = /(^|\/)\.env\.(example|sample|template|dist|defaults)$|(^|\/)(example|sample)\.env$/;
47
+ var REAL_ENV_NAMES = [".env", ".env.local", ".env.development", ".env.production", ".env.test", ".env.development.local", ".env.production.local"];
48
+ var CODE_REFS = [
49
+ [/\.(m|c)?(t|j)sx?$/, /\b(?:process\.env|import\.meta\.env)\.([A-Z][A-Z0-9_]{1,})|process\.env\[\s*["']([A-Z][A-Z0-9_]+)["']\s*\]/g],
50
+ [/\.py$/, /os\.(?:environ(?:\.get)?\s*[[(]\s*|getenv\s*\(\s*)["']([A-Z][A-Z0-9_]+)["']/g],
51
+ [/\.go$/, /os\.(?:Getenv|LookupEnv)\(\s*"([A-Z][A-Z0-9_]+)"/g],
52
+ [/\.rs$/, /env::var\(\s*"([A-Z][A-Z0-9_]+)"/g],
53
+ [/\.rb$/, /ENV\[\s*["']([A-Z][A-Z0-9_]+)["']\s*\]|ENV\.fetch\(\s*["']([A-Z][A-Z0-9_]+)["']/g],
54
+ [/\.php$/, /(?:getenv|env)\(\s*["']([A-Z][A-Z0-9_]+)["']/g],
55
+ [/\.(java|kt)$/, /System\.getenv\(\s*"([A-Z][A-Z0-9_]+)"/g],
56
+ [/\.cs$/, /GetEnvironmentVariable\(\s*"([A-Z][A-Z0-9_]+)"/g],
57
+ [/\.dart$/, /fromEnvironment\(\s*'([A-Z][A-Z0-9_]+)'/g]
58
+ ];
59
+ var TEST_DIR = /(^|\/)(test|tests|__tests__|spec)\//;
60
+ var templateKeysFact = {
61
+ id: "env-template-keys",
62
+ applies: (f) => TEMPLATE_ENV.test(f.path),
63
+ compute(text) {
64
+ if (!text) return [];
65
+ return [...text.matchAll(/^\s*(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=/gm)].map((m) => m[1]);
66
+ }
67
+ };
68
+ var CODE_REF_FACTS = CODE_REFS.map(([fileRe, refRe], i) => ({
69
+ id: `env-code-refs${i}`,
70
+ applies: (f) => fileRe.test(f.path) && !f.large && !f.binary && !TEST_DIR.test(f.path),
71
+ compute(text) {
72
+ const out = [];
73
+ if (!text) return out;
74
+ refRe.lastIndex = 0;
75
+ for (const m of text.matchAll(refRe)) {
76
+ const name = m[1] ?? m[2];
77
+ if (name && name !== "NODE_ENV") out.push(name);
78
+ }
79
+ return out;
80
+ }
81
+ }));
82
+ var envDetector = {
83
+ id: "env",
84
+ version: 1,
85
+ facts: [templateKeysFact, ...CODE_REF_FACTS],
86
+ async run(ctx) {
87
+ const vars = /* @__PURE__ */ new Map();
88
+ const add = (name, ref) => {
89
+ if (!vars.has(name)) vars.set(name, /* @__PURE__ */ new Set());
90
+ vars.get(name).add(ref);
91
+ };
92
+ for (const f of ctx.find(TEMPLATE_ENV)) {
93
+ const keys2 = await ctx.fact(f.path, templateKeysFact);
94
+ if (!keys2) continue;
95
+ for (const k of keys2) add(k, f.path);
96
+ }
97
+ let scanned = 0;
98
+ for (const [i, [fileRe]] of CODE_REFS.entries()) {
99
+ for (const f of ctx.find(fileRe)) {
100
+ if (f.large || f.binary || TEST_DIR.test(f.path)) continue;
101
+ if (++scanned > 2e4) break;
102
+ const names = await ctx.fact(f.path, CODE_REF_FACTS[i]);
103
+ if (!names) continue;
104
+ for (const name of names) add(name, f.path);
105
+ }
106
+ }
107
+ const present = /* @__PURE__ */ new Set();
108
+ for (const f of ctx.find(/(^|\/)\.env(\.[a-z.]+)?$/)) if (!TEMPLATE_ENV.test(f.path)) present.add(f.path);
109
+ for (const name of REAL_ENV_NAMES) {
110
+ try {
111
+ const st = await fs.stat(path.join(ctx.root, name));
112
+ if (st.isFile()) present.add(name);
113
+ } catch {
114
+ }
115
+ }
116
+ ctx.model.env.vars = [...vars.entries()].map(([name, refs]) => ({ name, references: [...refs].sort().slice(0, 10), secretLike: isSecretLikeName(name) })).sort((a, b) => a.name.localeCompare(b.name));
117
+ ctx.model.env.envFilesPresent = [...present].sort();
118
+ }
119
+ };
120
+
121
+ // src/core/analyzer/analyze.ts
122
+ import path5 from "path";
123
+ import { promises as fs5 } from "fs";
124
+
125
+ // src/core/model/project-model.ts
126
+ import { z as z2 } from "zod";
127
+
128
+ // src/core/model/fact.ts
129
+ import { z } from "zod";
130
+ var FactStatus = z.enum(["FACT", "DETECTED", "INFERRED", "UNKNOWN"]);
131
+ var Confidence = z.enum(["high", "medium", "low"]);
132
+ var FactSource = z.enum(["code", "config", "filesystem", "git", "developer", "ai"]);
133
+ var Evidence = z.object({
134
+ /** Repo-relative POSIX path. */
135
+ file: z.string(),
136
+ line: z.number().int().positive().optional(),
137
+ detail: z.string().optional()
138
+ });
139
+ var Provenance = z.object({
140
+ status: FactStatus,
141
+ confidence: Confidence,
142
+ source: FactSource,
143
+ evidence: z.array(Evidence)
144
+ });
145
+ function detected(source, evidence, confidence = "high") {
146
+ return { status: "DETECTED", confidence, source, evidence };
147
+ }
148
+ function inferred(source, evidence, confidence = "medium") {
149
+ return { status: "INFERRED", confidence, source, evidence };
150
+ }
151
+ function fact(source, evidence) {
152
+ return { status: "FACT", confidence: "high", source, evidence };
153
+ }
154
+
155
+ // src/core/model/project-model.ts
156
+ var Named = z2.object({ name: z2.string(), provenance: Provenance });
157
+ var Language = z2.object({
158
+ name: z2.string(),
159
+ files: z2.number().int(),
160
+ bytes: z2.number().int()
161
+ });
162
+ var Framework = Named.extend({
163
+ category: z2.enum(["frontend", "backend", "fullstack", "mobile", "desktop", "orm", "testing", "other"]),
164
+ version: z2.string().optional(),
165
+ /** Workspace package root (repo-relative) this belongs to; "." for root. */
166
+ root: z2.string()
167
+ });
168
+ var Manifest = z2.object({
169
+ path: z2.string(),
170
+ ecosystem: z2.string(),
171
+ name: z2.string().optional(),
172
+ version: z2.string().optional(),
173
+ dependencies: z2.array(z2.string()),
174
+ devDependencies: z2.array(z2.string())
175
+ });
176
+ var WorkspacePackage = z2.object({
177
+ name: z2.string(),
178
+ path: z2.string(),
179
+ kind: z2.enum(["app", "package", "root", "unknown"]),
180
+ ecosystem: z2.string(),
181
+ /** Names of other workspace packages this one depends on. */
182
+ internalDependencies: z2.array(z2.string())
183
+ });
184
+ var Command = z2.object({
185
+ name: z2.string(),
186
+ command: z2.string(),
187
+ source: z2.string(),
188
+ purpose: z2.enum(["dev", "build", "test", "lint", "format", "start", "migrate", "deploy", "other"])
189
+ });
190
+ var Route = z2.object({
191
+ method: z2.string(),
192
+ path: z2.string(),
193
+ framework: z2.string(),
194
+ file: z2.string(),
195
+ line: z2.number().int().optional(),
196
+ provenance: Provenance
197
+ });
198
+ var DbEntity = z2.object({
199
+ name: z2.string(),
200
+ kind: z2.enum(["table", "model", "collection"]),
201
+ fields: z2.array(z2.object({ name: z2.string(), type: z2.string().optional(), attributes: z2.array(z2.string()) })),
202
+ indexes: z2.array(z2.string()),
203
+ relations: z2.array(z2.string()),
204
+ file: z2.string(),
205
+ provenance: Provenance
206
+ });
207
+ var Database = Named.extend({ kind: z2.enum(["engine", "orm", "driver", "service"]) });
208
+ var EnvVar = z2.object({
209
+ name: z2.string(),
210
+ /** Where the name was seen. Values are NEVER recorded. */
211
+ references: z2.array(z2.string()),
212
+ secretLike: z2.boolean()
213
+ });
214
+ var SecretFinding = z2.object({
215
+ type: z2.string(),
216
+ file: z2.string(),
217
+ line: z2.number().int(),
218
+ /**
219
+ * Keyed fingerprint (HMAC with the machine-local salt in .athena/local.json, or a
220
+ * per-process key) — allows dedup without storing the secret. Never a plain hash
221
+ * of the value, and never written to committed files.
222
+ */
223
+ fingerprint: z2.string()
224
+ });
225
+ var CiJob = z2.object({ system: z2.string(), file: z2.string(), name: z2.string(), commands: z2.array(z2.string()) });
226
+ var ContainerService = z2.object({
227
+ name: z2.string(),
228
+ image: z2.string().optional(),
229
+ ports: z2.array(z2.string()),
230
+ dependsOn: z2.array(z2.string()),
231
+ file: z2.string()
232
+ });
233
+ var TestInfo = z2.object({
234
+ frameworks: z2.array(Named),
235
+ testFileCount: z2.number().int(),
236
+ testDirs: z2.array(z2.string()),
237
+ coverageConfigs: z2.array(z2.string()),
238
+ /** Workspace packages that contain source files but zero detected test files (structural, not coverage). */
239
+ packagesWithoutTests: z2.array(z2.string())
240
+ });
241
+ var GitInfo = z2.object({
242
+ available: z2.boolean(),
243
+ isRepo: z2.boolean(),
244
+ branch: z2.string().optional(),
245
+ head: z2.string().optional(),
246
+ lastCommitDate: z2.string().optional(),
247
+ contributorCount: z2.number().int().optional(),
248
+ hotspots: z2.array(z2.object({ path: z2.string(), commits: z2.number().int() }))
249
+ });
250
+ var ProjectModel = z2.object({
251
+ schemaVersion: z2.literal(1),
252
+ name: z2.string(),
253
+ description: z2.string().optional(),
254
+ root: z2.string(),
255
+ stats: z2.object({
256
+ filesScanned: z2.number().int(),
257
+ skippedBinary: z2.number().int(),
258
+ skippedLarge: z2.number().int(),
259
+ skippedUnreadable: z2.number().int()
260
+ }),
261
+ topLevelDirs: z2.array(z2.string()),
262
+ languages: z2.array(Language),
263
+ manifests: z2.array(Manifest),
264
+ packageManagers: z2.array(Named),
265
+ buildSystems: z2.array(Named),
266
+ workspace: z2.object({
267
+ isMonorepo: z2.boolean(),
268
+ tool: z2.string().optional(),
269
+ packages: z2.array(WorkspacePackage)
270
+ }),
271
+ frameworks: z2.array(Framework),
272
+ entryPoints: z2.array(z2.object({ path: z2.string(), provenance: Provenance })),
273
+ commands: z2.array(Command),
274
+ databases: z2.array(Database),
275
+ dbEntities: z2.array(DbEntity),
276
+ migrations: z2.array(z2.object({ path: z2.string(), tool: z2.string(), count: z2.number().int() })),
277
+ routes: z2.array(Route),
278
+ apiSpecs: z2.array(z2.object({ path: z2.string(), kind: z2.enum(["openapi", "graphql", "grpc"]) })),
279
+ auth: z2.array(Named.extend({ kind: z2.enum(["library", "provider", "hashing", "middleware", "token"]) })),
280
+ containers: z2.object({ dockerfiles: z2.array(z2.object({ path: z2.string(), baseImages: z2.array(z2.string()), exposedPorts: z2.array(z2.string()) })), services: z2.array(ContainerService) }),
281
+ infrastructure: z2.array(Named.extend({ kind: z2.string(), file: z2.string() })),
282
+ ci: z2.array(CiJob),
283
+ tests: TestInfo,
284
+ env: z2.object({ vars: z2.array(EnvVar), envFilesPresent: z2.array(z2.string()) }),
285
+ security: z2.object({
286
+ secrets: z2.array(SecretFinding),
287
+ tooling: z2.array(Named),
288
+ /** Libraries/middleware relevant to web security controls (helmet, cors, csrf...). */
289
+ controls: z2.array(Named)
290
+ }),
291
+ caching: z2.array(Named),
292
+ queues: z2.array(Named),
293
+ observability: z2.array(Named),
294
+ git: GitInfo,
295
+ warnings: z2.array(z2.string())
296
+ });
297
+ function emptyModel(name, root) {
298
+ return {
299
+ schemaVersion: 1,
300
+ name,
301
+ root,
302
+ stats: { filesScanned: 0, skippedBinary: 0, skippedLarge: 0, skippedUnreadable: 0 },
303
+ topLevelDirs: [],
304
+ languages: [],
305
+ manifests: [],
306
+ packageManagers: [],
307
+ buildSystems: [],
308
+ workspace: { isMonorepo: false, packages: [] },
309
+ frameworks: [],
310
+ entryPoints: [],
311
+ commands: [],
312
+ databases: [],
313
+ dbEntities: [],
314
+ migrations: [],
315
+ routes: [],
316
+ apiSpecs: [],
317
+ auth: [],
318
+ containers: { dockerfiles: [], services: [] },
319
+ infrastructure: [],
320
+ ci: [],
321
+ tests: { frameworks: [], testFileCount: 0, testDirs: [], coverageConfigs: [], packagesWithoutTests: [] },
322
+ env: { vars: [], envFilesPresent: [] },
323
+ security: { secrets: [], tooling: [], controls: [] },
324
+ caching: [],
325
+ queues: [],
326
+ observability: [],
327
+ git: { available: false, isRepo: false, hotspots: [] },
328
+ warnings: []
329
+ };
330
+ }
331
+
332
+ // src/core/analyzer/context.ts
333
+ function createContext(root, config, files, model, source, signal) {
334
+ const byPath = new Map(files.map((f) => [f.path, f]));
335
+ return {
336
+ root,
337
+ config,
338
+ files,
339
+ model,
340
+ signal,
341
+ runtime: source.runtime,
342
+ has: (rel) => byPath.has(rel),
343
+ read: (rel) => source.read(rel),
344
+ fact: (rel, def) => source.fact(rel, def),
345
+ find(match) {
346
+ if (match instanceof RegExp) return files.filter((f) => match.test(f.path));
347
+ return files.filter(match);
348
+ },
349
+ warn(message) {
350
+ model.warnings.push(message);
351
+ }
352
+ };
353
+ }
354
+
355
+ // src/core/analyzer/engine.ts
356
+ import { promises as fs2 } from "fs";
357
+ import path2 from "path";
358
+ import crypto from "crypto";
359
+
360
+ // src/core/cache/lru.ts
361
+ var ByteLRU = class {
362
+ constructor(budget, weigh) {
363
+ this.budget = budget;
364
+ this.weigh = weigh;
365
+ }
366
+ budget;
367
+ weigh;
368
+ map = /* @__PURE__ */ new Map();
369
+ total = 0;
370
+ get size() {
371
+ return this.map.size;
372
+ }
373
+ get bytes() {
374
+ return this.total;
375
+ }
376
+ has(key) {
377
+ return this.map.has(key);
378
+ }
379
+ get(key) {
380
+ const e = this.map.get(key);
381
+ if (!e) return void 0;
382
+ this.map.delete(key);
383
+ this.map.set(key, e);
384
+ return e.v;
385
+ }
386
+ set(key, v) {
387
+ const w = Math.max(1, this.weigh(v));
388
+ const old = this.map.get(key);
389
+ if (old) {
390
+ this.total -= old.w;
391
+ this.map.delete(key);
392
+ }
393
+ if (w > this.budget) return;
394
+ this.map.set(key, { v, w });
395
+ this.total += w;
396
+ for (const [k, e] of this.map) {
397
+ if (this.total <= this.budget) break;
398
+ this.map.delete(k);
399
+ this.total -= e.w;
400
+ }
401
+ }
402
+ clear() {
403
+ this.map.clear();
404
+ this.total = 0;
405
+ }
406
+ };
407
+ var DEFAULT_TEXT_CACHE_BYTES = 64 * 1024 * 1024;
408
+
409
+ // src/core/analyzer/engine.ts
410
+ var ERR = "$err";
411
+ var FactEngine = class {
412
+ constructor(opts) {
413
+ this.opts = opts;
414
+ this.byPath = new Map(opts.files.map((f) => [f.path, f]));
415
+ this.runtime = opts.runtime;
416
+ this.lru = new ByteLRU(opts.textCacheBytes ?? DEFAULT_TEXT_CACHE_BYTES, (s) => s.length * 2);
417
+ this.readFileFn = opts.readFile ?? ((abs) => fs2.readFile(abs));
418
+ }
419
+ opts;
420
+ byPath;
421
+ /** Files that could not be read in the content pass (dropped from the analysis). */
422
+ unreadable = /* @__PURE__ */ new Set();
423
+ stats = { filesRead: 0, rereads: 0, factsComputed: 0 };
424
+ hints = null;
425
+ runtime;
426
+ inflight = /* @__PURE__ */ new Map();
427
+ readPaths = /* @__PURE__ */ new Set();
428
+ lru;
429
+ readFileFn;
430
+ keyOf(def, rel) {
431
+ if (def.pathKeyed) return `${def.id}@${rel}`;
432
+ if (def.keyed) return `${def.id}:${this.runtime.fingerprintKeyId ?? "ephemeral"}`;
433
+ return def.id;
434
+ }
435
+ persistable(def) {
436
+ return !def.keyed || this.runtime.fingerprintKeyId !== null;
437
+ }
438
+ /** Facts computed for this file in the content pass, given the current hints. */
439
+ eagerDefs(f) {
440
+ return this.opts.defs.filter((d) => d.applies(f) && (!d.when || this.hints !== null && d.when(this.hints)));
441
+ }
442
+ /** True when the file must be read: unknown hash, or an applicable fact missing from the cache. */
443
+ needsProcessing(f) {
444
+ if (f.hash === "") return true;
445
+ if (f.binary || f.large) return false;
446
+ const entry = this.opts.cache.entry(f.hash);
447
+ for (const d of this.opts.defs) {
448
+ if (!d.applies(f) || d.when && (this.hints === null || !d.when(this.hints))) continue;
449
+ if (entry?.[this.keyOf(d, f.path)] === void 0 && !this.opts.cache.has(f.hash, this.keyOf(d, f.path))) return true;
450
+ }
451
+ return false;
452
+ }
453
+ async readBytes(rel) {
454
+ this.opts.signal?.throwIfAborted();
455
+ const buf = await this.readFileFn(path2.join(this.opts.root, rel));
456
+ this.stats.filesRead++;
457
+ if (this.readPaths.has(rel)) this.stats.rereads++;
458
+ else this.readPaths.add(rel);
459
+ return buf;
460
+ }
461
+ /** Content pass for one file: read once, hash, sniff, compute missing facts, drop the text. */
462
+ process(f, extra) {
463
+ let p = this.inflight.get(f.path);
464
+ if (!p) {
465
+ p = this.doProcess(f, extra);
466
+ this.inflight.set(f.path, p);
467
+ }
468
+ return p;
469
+ }
470
+ async doProcess(f, extra) {
471
+ let buf;
472
+ try {
473
+ buf = await this.readBytes(f.path);
474
+ } catch (err) {
475
+ if (this.opts.signal?.aborted) throw err;
476
+ if (f.hash === "") this.unreadable.add(f.path);
477
+ return;
478
+ }
479
+ f.hash = crypto.createHash("sha256").update(buf).digest("hex");
480
+ f.binary = sniffBinary(buf);
481
+ if (f.binary || f.large) return;
482
+ const text = buf.toString("utf8");
483
+ await this.opts.cache.preload([f.hash]);
484
+ const defs = this.eagerDefs(f);
485
+ if (extra && !defs.includes(extra)) defs.push(extra);
486
+ for (const def of defs) {
487
+ const key = this.keyOf(def, f.path);
488
+ if (!this.opts.cache.has(f.hash, key)) await this.computeAndStore(f, def, key, text);
489
+ }
490
+ }
491
+ async computeAndStore(f, def, key, text) {
492
+ let value;
493
+ try {
494
+ value = def.compute(text, f.path, this.runtime);
495
+ } catch (err) {
496
+ value = { [ERR]: err?.message ?? String(err) };
497
+ }
498
+ this.stats.factsComputed++;
499
+ await this.opts.cache.set(f.hash, key, value, { persist: this.persistable(def), replacePrefix: def.keyed ? `${def.id}:` : void 0 });
500
+ }
501
+ async read(rel) {
502
+ const cached = this.lru.get(rel);
503
+ if (cached !== void 0) return cached;
504
+ const entry = this.byPath.get(rel);
505
+ if (!entry || entry.binary || entry.large || this.unreadable.has(rel)) return null;
506
+ let text;
507
+ try {
508
+ text = (await this.readBytes(rel)).toString("utf8");
509
+ } catch (err) {
510
+ if (this.opts.signal?.aborted) throw err;
511
+ text = null;
512
+ }
513
+ if (text !== null) this.lru.set(rel, text);
514
+ return text;
515
+ }
516
+ async fact(rel, def) {
517
+ const f = this.byPath.get(rel);
518
+ if (!f) return null;
519
+ if (f.hash === "") await this.process(f);
520
+ if (f.binary || f.large || this.unreadable.has(rel)) return null;
521
+ const d = def;
522
+ const key = this.keyOf(d, rel);
523
+ let v = this.opts.cache.peek(f.hash, key);
524
+ if (v === void 0) v = await this.opts.cache.get(f.hash, key);
525
+ if (v === void 0 && !this.inflight.has(rel)) {
526
+ await this.process(f, d);
527
+ if (f.binary || f.large) return null;
528
+ v = await this.opts.cache.get(f.hash, key);
529
+ }
530
+ if (v === void 0) {
531
+ const text = await this.read(rel);
532
+ if (text === null) return null;
533
+ await this.computeAndStore(f, d, key, text);
534
+ v = await this.opts.cache.get(f.hash, key);
535
+ }
536
+ if (v && typeof v === "object" && !Array.isArray(v) && ERR in v) throw new Error(String(v[ERR]));
537
+ if (v === null || typeof v !== "object") return v;
538
+ if (Array.isArray(v) && v.length === 0) return [];
539
+ return structuredClone(v);
540
+ }
541
+ };
542
+
543
+ // src/core/analyzer/incremental.ts
544
+ import { promises as fs3 } from "fs";
545
+ import path3 from "path";
546
+ var MAX_INCREMENTAL_PATHS = 2e3;
547
+ function readExclude(root) {
548
+ return fs3.readFile(path3.join(root, ".git", "info", "exclude"), "utf8").catch(() => null);
549
+ }
550
+ function isIgnoredWithAncestors(matcher, rel, isDir2) {
551
+ for (let i = rel.indexOf("/"); i > 0; i = rel.indexOf("/", i + 1)) {
552
+ if (matcher.ignores(rel.slice(0, i), true)) return true;
553
+ }
554
+ return matcher.ignores(rel, isDir2);
555
+ }
556
+ async function incrementalWalk(root, config, configHash, input, signal) {
557
+ const { snapshot } = input;
558
+ if (!snapshot.reusable || snapshot.configHash !== configHash || snapshot.maxFiles !== config.maxFiles) return null;
559
+ const changed = [...new Set(input.changedPaths)];
560
+ if (changed.length > MAX_INCREMENTAL_PATHS) return null;
561
+ if (await readExclude(root) !== snapshot.excludeText) return null;
562
+ const byPath = new Map(snapshot.files.map((f) => [f.path, f]));
563
+ let dirPrefixes = null;
564
+ const isKnownDir = (rel) => {
565
+ if (!dirPrefixes) {
566
+ dirPrefixes = /* @__PURE__ */ new Set();
567
+ for (const f of snapshot.files) for (let i = f.path.indexOf("/"); i > 0; i = f.path.indexOf("/", i + 1)) dirPrefixes.add(f.path.slice(0, i));
568
+ }
569
+ return dirPrefixes.has(rel);
570
+ };
571
+ for (const rel of changed) {
572
+ signal?.throwIfAborted();
573
+ if (!rel || rel === "." || rel.startsWith("..") || path3.isAbsolute(rel) || rel.includes("\\")) return null;
574
+ const base = rel.slice(rel.lastIndexOf("/") + 1);
575
+ if (base === ".gitignore") return null;
576
+ let st;
577
+ try {
578
+ st = await fs3.lstat(path3.join(root, rel));
579
+ } catch (err) {
580
+ if (err.code !== "ENOENT" && err.code !== "ENOTDIR") return null;
581
+ if (byPath.delete(rel)) continue;
582
+ if (isKnownDir(rel)) return null;
583
+ continue;
584
+ }
585
+ if (st.isSymbolicLink()) return null;
586
+ if (st.isDirectory()) {
587
+ if (isIgnoredWithAncestors(snapshot.matcher, rel, true)) continue;
588
+ return null;
589
+ }
590
+ if (!st.isFile()) {
591
+ byPath.delete(rel);
592
+ continue;
593
+ }
594
+ if (isKnownDir(rel)) return null;
595
+ if (isIgnoredWithAncestors(snapshot.matcher, rel, false)) {
596
+ byPath.delete(rel);
597
+ continue;
598
+ }
599
+ const size = st.size;
600
+ const mtimeMs = Math.floor(st.mtimeMs);
601
+ byPath.set(
602
+ rel,
603
+ size > config.maxFileBytes ? { path: rel, size, mtimeMs, hash: `meta:${size}:${mtimeMs}`, binary: false, large: true } : { path: rel, size, mtimeMs, hash: "", binary: false, large: false }
604
+ );
605
+ }
606
+ if (byPath.size > config.maxFiles) return null;
607
+ const files = [...byPath.values()].map((f) => ({ ...f }));
608
+ files.sort((a, b) => a.path < b.path ? -1 : a.path > b.path ? 1 : 0);
609
+ return { files, skippedBinary: 0, skippedLarge: 0, skippedUnreadable: 0, truncated: false, warnings: [], symlinks: 0 };
610
+ }
611
+
612
+ // src/core/analyzer/detectors/structure.ts
613
+ var EXT_LANG = {
614
+ ts: "TypeScript",
615
+ tsx: "TypeScript",
616
+ mts: "TypeScript",
617
+ cts: "TypeScript",
618
+ js: "JavaScript",
619
+ jsx: "JavaScript",
620
+ mjs: "JavaScript",
621
+ cjs: "JavaScript",
622
+ py: "Python",
623
+ go: "Go",
624
+ rs: "Rust",
625
+ java: "Java",
626
+ kt: "Kotlin",
627
+ kts: "Kotlin",
628
+ scala: "Scala",
629
+ swift: "Swift",
630
+ m: "Objective-C",
631
+ mm: "Objective-C",
632
+ c: "C",
633
+ h: "C/C++ header",
634
+ cc: "C++",
635
+ cpp: "C++",
636
+ cxx: "C++",
637
+ hpp: "C++",
638
+ cs: "C#",
639
+ fs: "F#",
640
+ vb: "Visual Basic",
641
+ php: "PHP",
642
+ rb: "Ruby",
643
+ dart: "Dart",
644
+ ex: "Elixir",
645
+ exs: "Elixir",
646
+ erl: "Erlang",
647
+ clj: "Clojure",
648
+ hs: "Haskell",
649
+ lua: "Lua",
650
+ r: "R",
651
+ jl: "Julia",
652
+ zig: "Zig",
653
+ sol: "Solidity",
654
+ vue: "Vue",
655
+ svelte: "Svelte",
656
+ astro: "Astro",
657
+ html: "HTML",
658
+ css: "CSS",
659
+ scss: "SCSS",
660
+ sass: "Sass",
661
+ less: "Less",
662
+ sql: "SQL",
663
+ sh: "Shell",
664
+ bash: "Shell",
665
+ zsh: "Shell",
666
+ ps1: "PowerShell",
667
+ tf: "Terraform",
668
+ hcl: "HCL",
669
+ proto: "Protocol Buffers",
670
+ graphql: "GraphQL",
671
+ gql: "GraphQL",
672
+ prisma: "Prisma"
673
+ };
674
+ var structureDetector = {
675
+ id: "structure",
676
+ version: 1,
677
+ async run(ctx) {
678
+ const langs = /* @__PURE__ */ new Map();
679
+ const top = /* @__PURE__ */ new Set();
680
+ for (const f of ctx.files) {
681
+ const slash = f.path.indexOf("/");
682
+ if (slash > 0) top.add(f.path.slice(0, slash));
683
+ if (f.binary) continue;
684
+ const dot = f.path.lastIndexOf(".");
685
+ if (dot === -1 || dot < f.path.lastIndexOf("/")) continue;
686
+ const lang = EXT_LANG[f.path.slice(dot + 1).toLowerCase()];
687
+ if (!lang) continue;
688
+ const cur = langs.get(lang) ?? { files: 0, bytes: 0 };
689
+ cur.files++;
690
+ cur.bytes += f.size;
691
+ langs.set(lang, cur);
692
+ }
693
+ ctx.model.languages = [...langs.entries()].map(([name, v]) => ({ name, ...v })).sort((a, b) => b.bytes - a.bytes || a.name.localeCompare(b.name));
694
+ ctx.model.topLevelDirs = [...top].sort();
695
+ }
696
+ };
697
+
698
+ // src/core/analyzer/detectors/manifests/shared.ts
699
+ var isObj = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
700
+ var keys = (v) => isObj(v) ? Object.keys(v) : [];
701
+ function commandPurpose(name, cmd) {
702
+ const n = name.toLowerCase();
703
+ if (/^(dev|develop|serve|watch)(:|$)/.test(n)) return "dev";
704
+ if (/^build(:|$)/.test(n)) return "build";
705
+ if (/^(test|e2e|spec|coverage)(:|$)/.test(n) || /\b(jest|vitest|pytest|playwright|cypress)\b/.test(cmd)) return "test";
706
+ if (/^lint(:|$)|typecheck|type-check/.test(n)) return "lint";
707
+ if (/^(format|fmt|prettier)(:|$)/.test(n)) return "format";
708
+ if (/^start(:|$)/.test(n)) return "start";
709
+ if (/migrat|db:/.test(n)) return "migrate";
710
+ if (/deploy|release|publish/.test(n)) return "deploy";
711
+ return "other";
712
+ }
713
+
714
+ // src/core/analyzer/detectors/manifests/npm.ts
715
+ import { parse as parseYaml } from "yaml";
716
+ function parseNpm(text, p, warn) {
717
+ if (!text) return null;
718
+ let json;
719
+ try {
720
+ json = JSON.parse(text);
721
+ } catch {
722
+ warn(`Could not parse ${p} (invalid JSON)`);
723
+ return null;
724
+ }
725
+ const scripts = isObj(json.scripts) ? json.scripts : {};
726
+ const commands = Object.entries(scripts).filter(([, v]) => typeof v === "string").map(([name, v]) => ({ name, command: v, source: p, purpose: commandPurpose(name, v) }));
727
+ let workspaceGlobs;
728
+ if (Array.isArray(json.workspaces)) workspaceGlobs = json.workspaces.filter((x) => typeof x === "string");
729
+ else if (isObj(json.workspaces) && Array.isArray(json.workspaces.packages)) workspaceGlobs = json.workspaces.packages.filter((x) => typeof x === "string");
730
+ return {
731
+ manifest: {
732
+ path: p,
733
+ ecosystem: "npm",
734
+ name: typeof json.name === "string" ? json.name : void 0,
735
+ version: typeof json.version === "string" ? json.version : void 0,
736
+ dependencies: [...keys(json.dependencies), ...keys(json.peerDependencies)],
737
+ devDependencies: keys(json.devDependencies)
738
+ },
739
+ workspaceGlobs,
740
+ workspaceTool: workspaceGlobs ? "npm/yarn workspaces" : void 0,
741
+ commands
742
+ };
743
+ }
744
+ function parsePnpmWorkspace(text) {
745
+ if (!text) return void 0;
746
+ try {
747
+ const y = parseYaml(text);
748
+ return Array.isArray(y.packages) ? y.packages.filter((x) => typeof x === "string") : void 0;
749
+ } catch {
750
+ return void 0;
751
+ }
752
+ }
753
+ var rootPackageFact = {
754
+ id: "root-package",
755
+ applies: (f) => f.path === "package.json",
756
+ compute(text) {
757
+ const out = { pm: null, description: null, entries: [] };
758
+ if (!text) return out;
759
+ out.pm = /"packageManager"\s*:\s*"([a-z]+)@/.exec(text)?.[1] ?? null;
760
+ try {
761
+ const d = JSON.parse(text).description;
762
+ if (typeof d === "string" && d.trim()) out.description = d.trim();
763
+ } catch {
764
+ }
765
+ try {
766
+ const j = JSON.parse(text);
767
+ for (const field of ["main", "module"]) {
768
+ const v = j[field];
769
+ if (typeof v === "string") out.entries.push([v.replace(/^\.\//, ""), `"${field}" field`]);
770
+ }
771
+ if (isObj(j.bin)) {
772
+ for (const v of Object.values(j.bin)) if (typeof v === "string") out.entries.push([v.replace(/^\.\//, ""), '"bin" field']);
773
+ }
774
+ } catch {
775
+ }
776
+ return out;
777
+ }
778
+ };
779
+ var pnpmWorkspaceFact = {
780
+ id: "pnpm-workspace",
781
+ applies: (f) => f.path === "pnpm-workspace.yaml",
782
+ compute: (text) => parsePnpmWorkspace(text) ?? false
783
+ };
784
+
785
+ // src/core/analyzer/detectors/manifests/python.ts
786
+ import { parse as parseToml } from "smol-toml";
787
+ function pyReqName(spec) {
788
+ const m = /^\s*([A-Za-z0-9][A-Za-z0-9._-]*)/.exec(spec);
789
+ return m ? m[1].toLowerCase().replace(/_/g, "-") : null;
790
+ }
791
+ function parsePyproject(text, p, warn) {
792
+ if (!text) return null;
793
+ let t;
794
+ try {
795
+ t = parseToml(text);
796
+ } catch {
797
+ warn(`Could not parse ${p} (invalid TOML)`);
798
+ return null;
799
+ }
800
+ const project = isObj(t.project) ? t.project : {};
801
+ const tool = isObj(t.tool) ? t.tool : {};
802
+ const poetry = isObj(tool.poetry) ? tool.poetry : {};
803
+ const deps = /* @__PURE__ */ new Set();
804
+ const dev = /* @__PURE__ */ new Set();
805
+ for (const d of Array.isArray(project.dependencies) ? project.dependencies : []) {
806
+ const n = typeof d === "string" ? pyReqName(d) : null;
807
+ if (n) deps.add(n);
808
+ }
809
+ if (isObj(project["optional-dependencies"])) {
810
+ for (const group of Object.values(project["optional-dependencies"])) {
811
+ for (const d of Array.isArray(group) ? group : []) {
812
+ const n = typeof d === "string" ? pyReqName(d) : null;
813
+ if (n) dev.add(n);
814
+ }
815
+ }
816
+ }
817
+ if (isObj(t["dependency-groups"])) {
818
+ for (const group of Object.values(t["dependency-groups"])) {
819
+ for (const d of Array.isArray(group) ? group : []) {
820
+ const n = typeof d === "string" ? pyReqName(d) : null;
821
+ if (n) dev.add(n);
822
+ }
823
+ }
824
+ }
825
+ for (const k of keys(poetry.dependencies)) if (k.toLowerCase() !== "python") deps.add(k.toLowerCase());
826
+ for (const k of keys(poetry["dev-dependencies"])) dev.add(k.toLowerCase());
827
+ if (isObj(poetry.group)) {
828
+ for (const g of Object.values(poetry.group)) if (isObj(g)) for (const k of keys(g.dependencies)) dev.add(k.toLowerCase());
829
+ }
830
+ const commands = [];
831
+ const scripts = isObj(project.scripts) ? project.scripts : isObj(poetry.scripts) ? poetry.scripts : {};
832
+ for (const [name, v] of Object.entries(scripts)) if (typeof v === "string") commands.push({ name, command: v, source: p, purpose: commandPurpose(name, v) });
833
+ const uvWs = isObj(tool.uv) && isObj(tool.uv.workspace) && Array.isArray(tool.uv.workspace.members) ? tool.uv.workspace.members : void 0;
834
+ return {
835
+ manifest: {
836
+ path: p,
837
+ ecosystem: "python",
838
+ name: typeof project.name === "string" ? project.name : typeof poetry.name === "string" ? poetry.name : void 0,
839
+ version: typeof project.version === "string" ? project.version : void 0,
840
+ dependencies: [...deps],
841
+ devDependencies: [...dev]
842
+ },
843
+ workspaceGlobs: uvWs,
844
+ workspaceTool: uvWs ? "uv workspace" : void 0,
845
+ commands
846
+ };
847
+ }
848
+ function parseRequirements(text, p, warn) {
849
+ const deps = [];
850
+ for (const line of text.split(/\r?\n/)) {
851
+ const l = line.trim();
852
+ if (!l || l.startsWith("#") || l.startsWith("-")) continue;
853
+ const n = pyReqName(l);
854
+ if (n) deps.push(n);
855
+ }
856
+ const isDev = /dev|test|lint/i.test(baseName(p));
857
+ return { manifest: { path: p, ecosystem: "python", dependencies: isDev ? [] : deps, devDependencies: isDev ? deps : [] } };
858
+ }
859
+ function parsePipfile(text, p, warn) {
860
+ if (!text) return null;
861
+ try {
862
+ const t = parseToml(text);
863
+ return { manifest: { path: p, ecosystem: "python", dependencies: keys(t.packages).map((k) => k.toLowerCase()), devDependencies: keys(t["dev-packages"]).map((k) => k.toLowerCase()) } };
864
+ } catch {
865
+ warn(`Could not parse ${p}`);
866
+ return null;
867
+ }
868
+ }
869
+
870
+ // src/core/analyzer/detectors/manifests/go.ts
871
+ function parseGoMod(text, p, warn) {
872
+ if (!text) return null;
873
+ const mod = /^module\s+(\S+)/m.exec(text)?.[1];
874
+ const deps = /* @__PURE__ */ new Set();
875
+ const block = /require\s*\(([\s\S]*?)\)/g;
876
+ for (const m of text.matchAll(block)) for (const line of m[1].split("\n")) {
877
+ const dep = /^\s*(\S+)\s+v/.exec(line)?.[1];
878
+ if (dep) deps.add(dep);
879
+ }
880
+ for (const m of text.matchAll(/^require\s+(\S+)\s+v/gm)) deps.add(m[1]);
881
+ return { manifest: { path: p, ecosystem: "go", name: mod, dependencies: [...deps], devDependencies: [] } };
882
+ }
883
+ function parseGoWork(text, p, warn) {
884
+ if (!text) return null;
885
+ const members = [];
886
+ for (const m of text.matchAll(/use\s*\(([\s\S]*?)\)/g)) for (const l of m[1].split("\n")) if (l.trim()) members.push(l.trim().replace(/^\.\//, ""));
887
+ for (const m of text.matchAll(/^use\s+(\S+)/gm)) if (m[1] !== "(") members.push(m[1].replace(/^\.\//, ""));
888
+ return { manifest: { path: p, ecosystem: "go", dependencies: [], devDependencies: [] }, workspaceGlobs: members, workspaceTool: "go workspace" };
889
+ }
890
+
891
+ // src/core/analyzer/detectors/manifests/cargo.ts
892
+ import { parse as parseToml2 } from "smol-toml";
893
+ function parseCargo(text, p, warn) {
894
+ if (!text) return null;
895
+ let t;
896
+ try {
897
+ t = parseToml2(text);
898
+ } catch {
899
+ warn(`Could not parse ${p} (invalid TOML)`);
900
+ return null;
901
+ }
902
+ const pkg = isObj(t.package) ? t.package : {};
903
+ const ws = isObj(t.workspace) && Array.isArray(t.workspace.members) ? t.workspace.members : void 0;
904
+ return {
905
+ manifest: {
906
+ path: p,
907
+ ecosystem: "cargo",
908
+ name: typeof pkg.name === "string" ? pkg.name : void 0,
909
+ version: typeof pkg.version === "string" ? pkg.version : void 0,
910
+ dependencies: keys(t.dependencies),
911
+ devDependencies: keys(t["dev-dependencies"])
912
+ },
913
+ workspaceGlobs: ws,
914
+ workspaceTool: ws ? "cargo workspace" : void 0
915
+ };
916
+ }
917
+
918
+ // src/core/analyzer/detectors/manifests/maven.ts
919
+ function parsePom(text, p, warn) {
920
+ if (!text) return null;
921
+ const noParent = text.replace(/<parent>[\s\S]*?<\/parent>/, "").replace(/<dependencies>[\s\S]*<\/dependencies>/, "").replace(/<build>[\s\S]*<\/build>/, "");
922
+ const name = /<artifactId>([^<]+)<\/artifactId>/.exec(noParent)?.[1];
923
+ const deps = [];
924
+ const dev = [];
925
+ for (const m of text.matchAll(/<dependency>([\s\S]*?)<\/dependency>/g)) {
926
+ const a = /<artifactId>([^<]+)<\/artifactId>/.exec(m[1])?.[1];
927
+ if (!a) continue;
928
+ if (/<scope>\s*test\s*<\/scope>/.test(m[1])) dev.push(a);
929
+ else deps.push(a);
930
+ }
931
+ const modules = [...text.matchAll(/<module>([^<]+)<\/module>/g)].map((m) => m[1].trim());
932
+ return {
933
+ manifest: { path: p, ecosystem: "maven", name, dependencies: deps, devDependencies: dev },
934
+ workspaceGlobs: modules.length ? modules : void 0,
935
+ workspaceTool: modules.length ? "maven multi-module" : void 0
936
+ };
937
+ }
938
+
939
+ // src/core/analyzer/detectors/manifests/gradle.ts
940
+ function parseGradle(text, p, warn) {
941
+ if (!text) return null;
942
+ const deps = [];
943
+ const dev = [];
944
+ for (const m of text.matchAll(/\b(implementation|api|compileOnly|runtimeOnly|testImplementation|testRuntimeOnly|kapt|ksp|annotationProcessor)\s*\(?\s*["']([^"':]+):([^"':]+)(?::[^"']*)?["']/g)) {
945
+ (m[1].startsWith("test") ? dev : deps).push(m[3]);
946
+ }
947
+ return { manifest: { path: p, ecosystem: "gradle", dependencies: deps, devDependencies: dev } };
948
+ }
949
+ function parseGradleSettings(text, p, warn) {
950
+ if (!text) return null;
951
+ const members = [];
952
+ for (const m of text.matchAll(/include\s*\(?([^)\n]+)\)?/g)) {
953
+ for (const s of m[1].matchAll(/["']:?([^"']+)["']/g)) members.push(s[1].replace(/:/g, "/"));
954
+ }
955
+ const name = /rootProject\.name\s*=\s*["']([^"']+)["']/.exec(text)?.[1];
956
+ return {
957
+ manifest: { path: p, ecosystem: "gradle", name, dependencies: [], devDependencies: [] },
958
+ workspaceGlobs: members.length ? members : void 0,
959
+ workspaceTool: members.length ? "gradle multi-project" : void 0
960
+ };
961
+ }
962
+
963
+ // src/core/analyzer/detectors/manifests/composer.ts
964
+ function parseComposer(text, p, warn) {
965
+ if (!text) return null;
966
+ try {
967
+ const j = JSON.parse(text);
968
+ const scripts = isObj(j.scripts) ? j.scripts : {};
969
+ const commands = Object.entries(scripts).filter(([, v]) => typeof v === "string").map(([name, v]) => ({ name, command: v, source: p, purpose: commandPurpose(name, v) }));
970
+ return {
971
+ manifest: { path: p, ecosystem: "composer", name: typeof j.name === "string" ? j.name : void 0, dependencies: keys(j.require).filter((k) => k !== "php" && !k.startsWith("ext-")), devDependencies: keys(j["require-dev"]) },
972
+ commands
973
+ };
974
+ } catch {
975
+ warn(`Could not parse ${p} (invalid JSON)`);
976
+ return null;
977
+ }
978
+ }
979
+
980
+ // src/core/analyzer/detectors/manifests/dotnet.ts
981
+ function parseCsproj(text, p, warn) {
982
+ if (!text) return null;
983
+ const deps = [...text.matchAll(/<PackageReference\s+Include=["']([^"']+)["']/g)].map((m) => m[1]);
984
+ if (/Sdk=["']Microsoft\.NET\.Sdk\.Web["']/.test(text)) deps.push("Microsoft.AspNetCore.App");
985
+ const isTest = /xunit|nunit|mstest/i.test(deps.join(" "));
986
+ return { manifest: { path: p, ecosystem: "nuget", name: baseName(p).replace(/\.(cs|fs|vb)proj$/, ""), dependencies: isTest ? [] : deps, devDependencies: isTest ? deps : [] } };
987
+ }
988
+
989
+ // src/core/analyzer/detectors/manifests/ruby.ts
990
+ function parseGemfile(text, p, warn) {
991
+ if (!text) return null;
992
+ const deps = [];
993
+ const dev = [];
994
+ let inDevGroup = false;
995
+ for (const line of text.split(/\r?\n/)) {
996
+ if (/^\s*group\s+.*:(development|test)/.test(line)) inDevGroup = true;
997
+ else if (/^\s*end\b/.test(line)) inDevGroup = false;
998
+ const m = /^\s*gem\s+["']([^"']+)["']/.exec(line);
999
+ if (m) (inDevGroup ? dev : deps).push(m[1]);
1000
+ }
1001
+ return { manifest: { path: p, ecosystem: "rubygems", dependencies: deps, devDependencies: dev } };
1002
+ }
1003
+
1004
+ // src/core/analyzer/detectors/manifests/dart.ts
1005
+ import { parse as parseYaml2 } from "yaml";
1006
+ function parsePubspec(text, p, warn) {
1007
+ if (!text) return null;
1008
+ try {
1009
+ const y = parseYaml2(text);
1010
+ return { manifest: { path: p, ecosystem: "pub", name: typeof y.name === "string" ? y.name : void 0, dependencies: keys(y.dependencies), devDependencies: keys(y.dev_dependencies) } };
1011
+ } catch {
1012
+ warn(`Could not parse ${p} (invalid YAML)`);
1013
+ return null;
1014
+ }
1015
+ }
1016
+
1017
+ // src/core/analyzer/detectors/manifests/elixir.ts
1018
+ function parseMix(text, p, warn) {
1019
+ if (!text) return null;
1020
+ const deps = [...text.matchAll(/\{\s*:([a-z0-9_]+)\s*,/g)].map((m) => m[1]);
1021
+ const name = /app:\s*:([a-z0-9_]+)/.exec(text)?.[1];
1022
+ return { manifest: { path: p, ecosystem: "hex", name, dependencies: deps, devDependencies: [] } };
1023
+ }
1024
+
1025
+ // src/core/analyzer/detectors/manifests/tooling.ts
1026
+ var MAKE_RE = /(^|\/)(Makefile|makefile|justfile)$/;
1027
+ var makeTargetsFact = {
1028
+ id: "make-targets",
1029
+ applies: (f) => MAKE_RE.test(f.path) && f.path.split("/").length <= 3,
1030
+ compute(text) {
1031
+ const out = [];
1032
+ if (!text) return out;
1033
+ for (const m of text.matchAll(/^([A-Za-z0-9][A-Za-z0-9_.-]*)\s*:(?!=)/gm)) {
1034
+ const name = m[1];
1035
+ if (name === "PHONY" || name.startsWith(".")) continue;
1036
+ out.push(name);
1037
+ }
1038
+ return out;
1039
+ }
1040
+ };
1041
+ var LOCKFILES = [
1042
+ [/(^|\/)package-lock\.json$/, "npm"],
1043
+ [/(^|\/)npm-shrinkwrap\.json$/, "npm"],
1044
+ [/(^|\/)yarn\.lock$/, "yarn"],
1045
+ [/(^|\/)pnpm-lock\.yaml$/, "pnpm"],
1046
+ [/(^|\/)bun\.lockb?$/, "bun"],
1047
+ [/(^|\/)poetry\.lock$/, "poetry"],
1048
+ [/(^|\/)uv\.lock$/, "uv"],
1049
+ [/(^|\/)Pipfile\.lock$/, "pipenv"],
1050
+ [/(^|\/)pdm\.lock$/, "pdm"],
1051
+ [/(^|\/)go\.sum$/, "go modules"],
1052
+ [/(^|\/)Cargo\.lock$/, "cargo"],
1053
+ [/(^|\/)composer\.lock$/, "composer"],
1054
+ [/(^|\/)Gemfile\.lock$/, "bundler"],
1055
+ [/(^|\/)pubspec\.lock$/, "pub"],
1056
+ [/(^|\/)mix\.lock$/, "mix"],
1057
+ [/(^|\/)packages\.lock\.json$/, "nuget"]
1058
+ ];
1059
+ var BUILD_SYSTEMS = [
1060
+ [/(^|\/)turbo\.json$/, "Turborepo"],
1061
+ [/(^|\/)nx\.json$/, "Nx"],
1062
+ [/(^|\/)lerna\.json$/, "Lerna"],
1063
+ [/(^|\/)(vite|vitest)\.config\.(js|ts|mjs|cjs)$/, "Vite"],
1064
+ [/(^|\/)webpack\.config\.(js|ts|cjs|mjs)$/, "webpack"],
1065
+ [/(^|\/)rollup\.config\.(js|ts|mjs)$/, "Rollup"],
1066
+ [/(^|\/)tsup\.config\.(js|ts)$/, "tsup"],
1067
+ [/(^|\/)tsconfig\.json$/, "TypeScript compiler"],
1068
+ [/(^|\/)(Makefile|makefile|GNUmakefile)$/, "Make"],
1069
+ [/(^|\/)justfile$/i, "just"],
1070
+ [/(^|\/)Taskfile\.ya?ml$/, "Task"],
1071
+ [/(^|\/)CMakeLists\.txt$/, "CMake"],
1072
+ [/(^|\/)BUILD(\.bazel)?$|(^|\/)WORKSPACE(\.bazel)?$|(^|\/)MODULE\.bazel$/, "Bazel"],
1073
+ [/(^|\/)build\.gradle(\.kts)?$/, "Gradle"],
1074
+ [/(^|\/)pom\.xml$/, "Maven"],
1075
+ [/\.sln$/, ".NET solution"]
1076
+ ];
1077
+
1078
+ // src/core/analyzer/detectors/manifests/workspace.ts
1079
+ import ignoreFactory from "ignore";
1080
+ async function detectWorkspace(ctx, parsed) {
1081
+ const { model } = ctx;
1082
+ const globs = [];
1083
+ let tool;
1084
+ for (const p of parsed) {
1085
+ if (p.workspaceGlobs?.length && dirOf(p.manifest.path) === ".") {
1086
+ globs.push(...p.workspaceGlobs);
1087
+ tool ??= p.workspaceTool;
1088
+ }
1089
+ }
1090
+ if (ctx.has("pnpm-workspace.yaml")) {
1091
+ const g = await ctx.fact("pnpm-workspace.yaml", pnpmWorkspaceFact) || void 0;
1092
+ if (g?.length) {
1093
+ globs.push(...g);
1094
+ tool = "pnpm workspaces";
1095
+ }
1096
+ }
1097
+ const markerTool = ctx.has("turbo.json") ? "Turborepo" : ctx.has("nx.json") ? "Nx" : ctx.has("lerna.json") ? "Lerna" : void 0;
1098
+ if (markerTool) tool = tool ? `${tool} + ${markerTool}` : markerTool;
1099
+ const manifestDirs = /* @__PURE__ */ new Map();
1100
+ for (const m of model.manifests) {
1101
+ const d = dirOf(m.path);
1102
+ manifestDirs.set(d, [...manifestDirs.get(d) ?? [], m]);
1103
+ }
1104
+ const matcher = globs.length ? ignoreFactory().add(globs.map((g) => g.replace(/^\.\//, "").replace(/\/$/, ""))) : null;
1105
+ const pkgDirs = [...manifestDirs.keys()].filter((d) => d !== "." && (matcher ? matcher.ignores(d) : true));
1106
+ const isMonorepo = globs.length > 0 || !!markerTool || pkgDirs.length >= 2 && !manifestDirs.has(".");
1107
+ model.workspace.isMonorepo = isMonorepo;
1108
+ model.workspace.tool = tool;
1109
+ const packages = [];
1110
+ const dirsForPackages = isMonorepo ? [".", ...pkgDirs] : manifestDirs.has(".") ? ["."] : pkgDirs;
1111
+ for (const d of dirsForPackages) {
1112
+ const ms = manifestDirs.get(d);
1113
+ if (!ms) continue;
1114
+ const primary = ms.find((m) => m.name) ?? ms[0];
1115
+ const seg = d.split("/")[0] ?? "";
1116
+ const kind = d === "." ? "root" : /^(apps|services|examples)$/.test(seg) ? "app" : /^(packages|libs|crates|modules|shared|internal)$/.test(seg) ? "package" : "unknown";
1117
+ packages.push({ name: primary.name ?? (d === "." ? model.name : baseName(d)), path: d, kind, ecosystem: primary.ecosystem, internalDependencies: [] });
1118
+ }
1119
+ const names = new Set(packages.map((p) => p.name));
1120
+ for (const pkg of packages) {
1121
+ const deps = (manifestDirs.get(pkg.path) ?? []).flatMap((m) => [...m.dependencies, ...m.devDependencies]);
1122
+ pkg.internalDependencies = [...new Set(deps.filter((d) => names.has(d) && d !== pkg.name))].sort();
1123
+ }
1124
+ model.workspace.packages = packages;
1125
+ }
1126
+
1127
+ // src/core/analyzer/detectors/manifests/index.ts
1128
+ var PARSERS = [
1129
+ [/(^|\/)package\.json$/, parseNpm],
1130
+ [/(^|\/)pyproject\.toml$/, parsePyproject],
1131
+ [/(^|\/)requirements[^/]*\.txt$/, parseRequirements],
1132
+ [/(^|\/)Pipfile$/, parsePipfile],
1133
+ [/(^|\/)go\.mod$/, parseGoMod],
1134
+ [/(^|\/)go\.work$/, parseGoWork],
1135
+ [/(^|\/)Cargo\.toml$/, parseCargo],
1136
+ [/(^|\/)pom\.xml$/, parsePom],
1137
+ [/(^|\/)build\.gradle(\.kts)?$/, parseGradle],
1138
+ [/(^|\/)settings\.gradle(\.kts)?$/, parseGradleSettings],
1139
+ [/(^|\/)composer\.json$/, parseComposer],
1140
+ [/\.(cs|fs|vb)proj$/, parseCsproj],
1141
+ [/(^|\/)Gemfile$/, parseGemfile],
1142
+ [/(^|\/)pubspec\.yaml$/, parsePubspec],
1143
+ [/(^|\/)mix\.exs$/, parseMix]
1144
+ ];
1145
+ var MANIFEST_FACTS = PARSERS.map(([re, parser], i) => ({
1146
+ id: `manifest${i}`,
1147
+ pathKeyed: true,
1148
+ applies: (f) => re.test(f.path),
1149
+ compute(text, rel) {
1150
+ const w = [];
1151
+ const r = parser(text, rel, (m) => w.push(m));
1152
+ return { r, w };
1153
+ }
1154
+ }));
1155
+ async function manifestDependencyNames(p, text) {
1156
+ const parser = PARSERS.find(([re]) => re.test(p))?.[1];
1157
+ if (!parser) return null;
1158
+ if (!text.trim()) return /* @__PURE__ */ new Set();
1159
+ let failed = false;
1160
+ const r = parser(text, p, () => failed = true);
1161
+ if (!r || failed) return null;
1162
+ return /* @__PURE__ */ new Set([...r.manifest.dependencies, ...r.manifest.devDependencies]);
1163
+ }
1164
+ var manifestsDetector = {
1165
+ id: "manifests",
1166
+ version: 1,
1167
+ facts: [...MANIFEST_FACTS, makeTargetsFact, rootPackageFact, pnpmWorkspaceFact],
1168
+ async run(ctx) {
1169
+ const { model } = ctx;
1170
+ const parsed = [];
1171
+ for (const [i, [re]] of PARSERS.entries()) {
1172
+ for (const f of ctx.find(re)) {
1173
+ ctx.signal?.throwIfAborted();
1174
+ const v = await ctx.fact(f.path, MANIFEST_FACTS[i]);
1175
+ if (!v) continue;
1176
+ for (const w of v.w) ctx.warn(w);
1177
+ if (v.r) parsed.push(v.r);
1178
+ }
1179
+ }
1180
+ parsed.sort((a, b) => a.manifest.path.localeCompare(b.manifest.path));
1181
+ model.manifests = parsed.map((p) => p.manifest);
1182
+ model.commands.push(...parsed.flatMap((p) => p.commands ?? []));
1183
+ for (const f of ctx.find(MAKE_RE)) {
1184
+ if (f.path.split("/").length > 3) continue;
1185
+ const names = await ctx.fact(f.path, makeTargetsFact);
1186
+ if (!names) continue;
1187
+ const isJust = baseName(f.path) === "justfile";
1188
+ for (const name of names) model.commands.push({ name, command: isJust ? `just ${name}` : `make ${name}`, source: f.path, purpose: commandPurpose(name, "") });
1189
+ }
1190
+ const pm = /* @__PURE__ */ new Map();
1191
+ for (const [re, name] of LOCKFILES) for (const f of ctx.find(re)) pm.set(name, [...pm.get(name) ?? [], f.path]);
1192
+ for (const m of parsed) {
1193
+ const eco = m.manifest.ecosystem;
1194
+ const implied = eco === "python" && baseName(m.manifest.path).startsWith("requirements") ? "pip" : eco === "maven" ? "maven" : eco === "gradle" ? "gradle" : eco === "nuget" ? "dotnet" : null;
1195
+ if (implied && !pm.has(implied)) pm.set(implied, [m.manifest.path]);
1196
+ }
1197
+ const rootPkg = parsed.find((p) => p.manifest.path === "package.json");
1198
+ const rootMeta = rootPkg ? await ctx.fact("package.json", rootPackageFact) : null;
1199
+ if (rootMeta) {
1200
+ const declared = rootMeta.pm;
1201
+ if (declared && !pm.has(declared)) pm.set(declared, ["package.json"]);
1202
+ }
1203
+ model.packageManagers = [...pm.entries()].sort(([a], [b]) => a.localeCompare(b)).map(([name, files]) => ({
1204
+ name,
1205
+ provenance: detected("filesystem", files.slice(0, 5).map((file) => ({ file })))
1206
+ }));
1207
+ const bs = /* @__PURE__ */ new Map();
1208
+ for (const [re, name] of BUILD_SYSTEMS) for (const f of ctx.find(re)) bs.set(name, [...bs.get(name) ?? [], f.path]);
1209
+ model.buildSystems = [...bs.entries()].sort(([a], [b]) => a.localeCompare(b)).map(([name, files]) => ({
1210
+ name,
1211
+ provenance: detected("filesystem", files.slice(0, 5).map((file) => ({ file })))
1212
+ }));
1213
+ await detectWorkspace(ctx, parsed);
1214
+ const rootManifest = model.manifests.find((m) => dirOf(m.path) === "." && m.name);
1215
+ if (rootManifest?.name) model.name = rootManifest.name;
1216
+ if (rootMeta) {
1217
+ if (rootMeta.description) model.description = rootMeta.description;
1218
+ for (const [p, detail] of rootMeta.entries) model.entryPoints.push({ path: p, provenance: fact("config", [{ file: "package.json", detail }]) });
1219
+ }
1220
+ }
1221
+ };
1222
+
1223
+ // src/core/analyzer/catalog.ts
1224
+ var fw = (category) => ({ type: "framework", category });
1225
+ var db = (kind) => ({ type: "database", kind });
1226
+ var auth = (kind) => ({ type: "auth", kind });
1227
+ var CACHE = { type: "cache" };
1228
+ var QUEUE = { type: "queue" };
1229
+ var OBS = { type: "observability" };
1230
+ var SEC = { type: "securityControl" };
1231
+ var SECTOOL = { type: "securityTooling" };
1232
+ var TEST = { type: "testing" };
1233
+ var CATALOG = {
1234
+ npm: {
1235
+ next: { name: "Next.js", kind: fw("fullstack"), markers: [/(^|\/)next\.config\.(js|mjs|cjs|ts)$/] },
1236
+ react: { name: "React", kind: fw("frontend") },
1237
+ vue: { name: "Vue", kind: fw("frontend") },
1238
+ nuxt: { name: "Nuxt", kind: fw("fullstack"), markers: [/(^|\/)nuxt\.config\.(js|ts|mjs)$/] },
1239
+ "@angular/core": { name: "Angular", kind: fw("frontend"), markers: [/(^|\/)angular\.json$/] },
1240
+ svelte: { name: "Svelte", kind: fw("frontend") },
1241
+ "@sveltejs/kit": { name: "SvelteKit", kind: fw("fullstack"), markers: [/(^|\/)svelte\.config\.(js|ts)$/] },
1242
+ "solid-js": { name: "SolidJS", kind: fw("frontend") },
1243
+ astro: { name: "Astro", kind: fw("fullstack"), markers: [/(^|\/)astro\.config\.(mjs|ts|js)$/] },
1244
+ "@remix-run/react": { name: "Remix", kind: fw("fullstack") },
1245
+ "@tanstack/react-start": { name: "TanStack Start", kind: fw("fullstack") },
1246
+ "@tanstack/solid-start": { name: "TanStack Start", kind: fw("fullstack") },
1247
+ "@tanstack/react-router": { name: "TanStack Router", kind: fw("frontend") },
1248
+ "@tanstack/react-query": { name: "TanStack Query", kind: fw("frontend") },
1249
+ "@vercel/node": { name: "Vercel Functions (Node)", kind: fw("backend") },
1250
+ "react-router": { name: "React Router", kind: fw("frontend") },
1251
+ vite: { name: "Vite", kind: fw("other"), markers: [/(^|\/)vite\.config\.(js|ts|mjs)$/] },
1252
+ "react-native": { name: "React Native", kind: fw("mobile") },
1253
+ expo: { name: "Expo", kind: fw("mobile"), markers: [/(^|\/)app\.json$/] },
1254
+ electron: { name: "Electron", kind: fw("desktop") },
1255
+ "@tauri-apps/api": { name: "Tauri", kind: fw("desktop") },
1256
+ express: { name: "Express", kind: fw("backend") },
1257
+ fastify: { name: "Fastify", kind: fw("backend") },
1258
+ koa: { name: "Koa", kind: fw("backend") },
1259
+ hono: { name: "Hono", kind: fw("backend") },
1260
+ "@nestjs/core": { name: "NestJS", kind: fw("backend"), markers: [/(^|\/)nest-cli\.json$/] },
1261
+ "@trpc/server": { name: "tRPC", kind: fw("backend") },
1262
+ "@apollo/server": { name: "Apollo Server", kind: fw("backend") },
1263
+ "apollo-server": { name: "Apollo Server", kind: fw("backend") },
1264
+ graphql: { name: "GraphQL", kind: fw("other") },
1265
+ tailwindcss: { name: "Tailwind CSS", kind: fw("frontend") },
1266
+ prisma: { name: "Prisma", kind: db("orm"), markers: [/(^|\/)schema\.prisma$/] },
1267
+ "@prisma/client": { name: "Prisma", kind: db("orm"), markers: [/(^|\/)schema\.prisma$/] },
1268
+ "drizzle-orm": { name: "Drizzle ORM", kind: db("orm"), markers: [/(^|\/)drizzle\.config\.(ts|js)$/] },
1269
+ typeorm: { name: "TypeORM", kind: db("orm") },
1270
+ sequelize: { name: "Sequelize", kind: db("orm") },
1271
+ mongoose: { name: "Mongoose", kind: db("orm") },
1272
+ knex: { name: "Knex", kind: db("orm") },
1273
+ kysely: { name: "Kysely", kind: db("orm") },
1274
+ "@mikro-orm/core": { name: "MikroORM", kind: db("orm") },
1275
+ pg: { name: "PostgreSQL (pg driver)", kind: db("driver") },
1276
+ postgres: { name: "PostgreSQL (postgres.js driver)", kind: db("driver") },
1277
+ mysql: { name: "MySQL driver", kind: db("driver") },
1278
+ mysql2: { name: "MySQL driver", kind: db("driver") },
1279
+ "better-sqlite3": { name: "SQLite (better-sqlite3)", kind: db("driver") },
1280
+ sqlite3: { name: "SQLite driver", kind: db("driver") },
1281
+ mongodb: { name: "MongoDB driver", kind: db("driver") },
1282
+ "@supabase/supabase-js": { name: "Supabase", kind: db("driver") },
1283
+ firebase: { name: "Firebase", kind: db("driver") },
1284
+ "firebase-admin": { name: "Firebase Admin", kind: db("driver") },
1285
+ "@planetscale/database": { name: "PlanetScale driver", kind: db("driver") },
1286
+ "@neondatabase/serverless": { name: "Neon serverless driver", kind: db("driver") },
1287
+ "next-auth": { name: "NextAuth.js / Auth.js", kind: auth("library") },
1288
+ "@auth/core": { name: "Auth.js", kind: auth("library") },
1289
+ "better-auth": { name: "Better Auth", kind: auth("library") },
1290
+ passport: { name: "Passport", kind: auth("library") },
1291
+ lucia: { name: "Lucia", kind: auth("library") },
1292
+ "@clerk/nextjs": { name: "Clerk", kind: auth("provider") },
1293
+ "@clerk/clerk-react": { name: "Clerk", kind: auth("provider") },
1294
+ "@auth0/nextjs-auth0": { name: "Auth0", kind: auth("provider") },
1295
+ "auth0": { name: "Auth0", kind: auth("provider") },
1296
+ "@supabase/auth-helpers-nextjs": { name: "Supabase Auth", kind: auth("provider") },
1297
+ "@supabase/ssr": { name: "Supabase SSR Auth", kind: auth("provider") },
1298
+ "firebase-auth": { name: "Firebase Auth", kind: auth("provider") },
1299
+ jsonwebtoken: { name: "jsonwebtoken (JWT)", kind: auth("token") },
1300
+ jose: { name: "jose (JWT/JWE)", kind: auth("token") },
1301
+ "express-session": { name: "express-session", kind: auth("library") },
1302
+ "iron-session": { name: "iron-session", kind: auth("library") },
1303
+ bcrypt: { name: "bcrypt", kind: auth("hashing") },
1304
+ bcryptjs: { name: "bcryptjs", kind: auth("hashing") },
1305
+ argon2: { name: "argon2", kind: auth("hashing") },
1306
+ "@node-rs/argon2": { name: "argon2", kind: auth("hashing") },
1307
+ ioredis: { name: "Redis (ioredis)", kind: CACHE },
1308
+ redis: { name: "Redis client", kind: CACHE },
1309
+ "@upstash/redis": { name: "Upstash Redis", kind: CACHE },
1310
+ "lru-cache": { name: "lru-cache", kind: CACHE },
1311
+ "node-cache": { name: "node-cache", kind: CACHE },
1312
+ bullmq: { name: "BullMQ", kind: QUEUE },
1313
+ bull: { name: "Bull", kind: QUEUE },
1314
+ "bee-queue": { name: "Bee-Queue", kind: QUEUE },
1315
+ amqplib: { name: "RabbitMQ (amqplib)", kind: QUEUE },
1316
+ kafkajs: { name: "Kafka (kafkajs)", kind: QUEUE },
1317
+ "@aws-sdk/client-sqs": { name: "AWS SQS", kind: QUEUE },
1318
+ inngest: { name: "Inngest", kind: QUEUE },
1319
+ "@trigger.dev/sdk": { name: "Trigger.dev", kind: QUEUE },
1320
+ "@sentry/node": { name: "Sentry", kind: OBS },
1321
+ "@sentry/nextjs": { name: "Sentry", kind: OBS },
1322
+ "@sentry/react": { name: "Sentry", kind: OBS },
1323
+ pino: { name: "Pino logger", kind: OBS },
1324
+ winston: { name: "Winston logger", kind: OBS },
1325
+ "@opentelemetry/api": { name: "OpenTelemetry", kind: OBS },
1326
+ "dd-trace": { name: "Datadog APM", kind: OBS },
1327
+ helmet: { name: "helmet", kind: SEC },
1328
+ cors: { name: "cors", kind: SEC },
1329
+ "@fastify/cors": { name: "@fastify/cors", kind: SEC },
1330
+ "@fastify/helmet": { name: "@fastify/helmet", kind: SEC },
1331
+ csurf: { name: "csurf (deprecated)", kind: SEC },
1332
+ "csrf-csrf": { name: "csrf-csrf", kind: SEC },
1333
+ "express-rate-limit": { name: "express-rate-limit", kind: SEC },
1334
+ "@fastify/rate-limit": { name: "@fastify/rate-limit", kind: SEC },
1335
+ zod: { name: "zod (schema validation)", kind: SEC },
1336
+ joi: { name: "joi (schema validation)", kind: SEC },
1337
+ yup: { name: "yup (schema validation)", kind: SEC },
1338
+ "class-validator": { name: "class-validator", kind: SEC },
1339
+ dompurify: { name: "DOMPurify", kind: SEC },
1340
+ "isomorphic-dompurify": { name: "DOMPurify", kind: SEC },
1341
+ eslint: { name: "ESLint", kind: SECTOOL },
1342
+ "eslint-plugin-security": { name: "eslint-plugin-security", kind: SECTOOL },
1343
+ snyk: { name: "Snyk", kind: SECTOOL },
1344
+ jest: { name: "Jest", kind: TEST },
1345
+ vitest: { name: "Vitest", kind: TEST },
1346
+ mocha: { name: "Mocha", kind: TEST },
1347
+ ava: { name: "AVA", kind: TEST },
1348
+ "@playwright/test": { name: "Playwright", kind: TEST },
1349
+ cypress: { name: "Cypress", kind: TEST },
1350
+ "@testing-library/react": { name: "Testing Library", kind: TEST },
1351
+ supertest: { name: "Supertest", kind: TEST },
1352
+ detox: { name: "Detox", kind: TEST }
1353
+ },
1354
+ python: {
1355
+ django: { name: "Django", kind: fw("fullstack"), markers: [/(^|\/)manage\.py$/] },
1356
+ djangorestframework: { name: "Django REST Framework", kind: fw("backend") },
1357
+ fastapi: { name: "FastAPI", kind: fw("backend") },
1358
+ flask: { name: "Flask", kind: fw("backend") },
1359
+ starlette: { name: "Starlette", kind: fw("backend") },
1360
+ litestar: { name: "Litestar", kind: fw("backend") },
1361
+ tornado: { name: "Tornado", kind: fw("backend") },
1362
+ aiohttp: { name: "aiohttp", kind: fw("backend") },
1363
+ streamlit: { name: "Streamlit", kind: fw("frontend") },
1364
+ pydantic: { name: "Pydantic (validation)", kind: SEC },
1365
+ sqlalchemy: { name: "SQLAlchemy", kind: db("orm") },
1366
+ sqlmodel: { name: "SQLModel", kind: db("orm") },
1367
+ "tortoise-orm": { name: "Tortoise ORM", kind: db("orm") },
1368
+ peewee: { name: "Peewee", kind: db("orm") },
1369
+ alembic: { name: "Alembic migrations", kind: db("orm") },
1370
+ "psycopg2": { name: "PostgreSQL (psycopg2)", kind: db("driver") },
1371
+ "psycopg2-binary": { name: "PostgreSQL (psycopg2)", kind: db("driver") },
1372
+ psycopg: { name: "PostgreSQL (psycopg 3)", kind: db("driver") },
1373
+ asyncpg: { name: "PostgreSQL (asyncpg)", kind: db("driver") },
1374
+ pymysql: { name: "MySQL (PyMySQL)", kind: db("driver") },
1375
+ mysqlclient: { name: "MySQL (mysqlclient)", kind: db("driver") },
1376
+ pymongo: { name: "MongoDB (pymongo)", kind: db("driver") },
1377
+ motor: { name: "MongoDB (motor)", kind: db("driver") },
1378
+ "django-allauth": { name: "django-allauth", kind: auth("library") },
1379
+ "djangorestframework-simplejwt": { name: "Simple JWT (DRF)", kind: auth("token") },
1380
+ pyjwt: { name: "PyJWT", kind: auth("token") },
1381
+ "python-jose": { name: "python-jose", kind: auth("token") },
1382
+ authlib: { name: "Authlib", kind: auth("library") },
1383
+ "flask-login": { name: "Flask-Login", kind: auth("library") },
1384
+ "fastapi-users": { name: "FastAPI Users", kind: auth("library") },
1385
+ passlib: { name: "passlib", kind: auth("hashing") },
1386
+ bcrypt: { name: "bcrypt", kind: auth("hashing") },
1387
+ "argon2-cffi": { name: "argon2-cffi", kind: auth("hashing") },
1388
+ redis: { name: "Redis client", kind: CACHE },
1389
+ "django-redis": { name: "django-redis", kind: CACHE },
1390
+ celery: { name: "Celery", kind: QUEUE },
1391
+ rq: { name: "RQ", kind: QUEUE },
1392
+ dramatiq: { name: "Dramatiq", kind: QUEUE },
1393
+ "sentry-sdk": { name: "Sentry", kind: OBS },
1394
+ structlog: { name: "structlog", kind: OBS },
1395
+ "opentelemetry-api": { name: "OpenTelemetry", kind: OBS },
1396
+ "django-cors-headers": { name: "django-cors-headers", kind: SEC },
1397
+ bandit: { name: "Bandit", kind: SECTOOL },
1398
+ "pip-audit": { name: "pip-audit", kind: SECTOOL },
1399
+ safety: { name: "Safety", kind: SECTOOL },
1400
+ ruff: { name: "Ruff", kind: SECTOOL },
1401
+ pytest: { name: "pytest", kind: TEST },
1402
+ "pytest-django": { name: "pytest-django", kind: TEST },
1403
+ hypothesis: { name: "Hypothesis", kind: TEST },
1404
+ playwright: { name: "Playwright", kind: TEST }
1405
+ },
1406
+ go: {
1407
+ "github.com/gin-gonic/gin": { name: "Gin", kind: fw("backend") },
1408
+ "github.com/labstack/echo/v4": { name: "Echo", kind: fw("backend") },
1409
+ "github.com/gofiber/fiber/v2": { name: "Fiber", kind: fw("backend") },
1410
+ "github.com/gofiber/fiber/v3": { name: "Fiber", kind: fw("backend") },
1411
+ "github.com/go-chi/chi/v5": { name: "chi", kind: fw("backend") },
1412
+ "github.com/gorilla/mux": { name: "gorilla/mux", kind: fw("backend") },
1413
+ "google.golang.org/grpc": { name: "gRPC", kind: fw("backend") },
1414
+ "gorm.io/gorm": { name: "GORM", kind: db("orm") },
1415
+ "entgo.io/ent": { name: "ent", kind: db("orm") },
1416
+ "github.com/jmoiron/sqlx": { name: "sqlx", kind: db("driver") },
1417
+ "github.com/jackc/pgx/v5": { name: "PostgreSQL (pgx)", kind: db("driver") },
1418
+ "github.com/lib/pq": { name: "PostgreSQL (lib/pq)", kind: db("driver") },
1419
+ "github.com/go-sql-driver/mysql": { name: "MySQL driver", kind: db("driver") },
1420
+ "go.mongodb.org/mongo-driver": { name: "MongoDB driver", kind: db("driver") },
1421
+ "github.com/golang-migrate/migrate/v4": { name: "golang-migrate", kind: db("orm") },
1422
+ "github.com/golang-jwt/jwt/v5": { name: "golang-jwt", kind: auth("token") },
1423
+ "golang.org/x/crypto": { name: "x/crypto (may include bcrypt/argon2)", kind: auth("hashing") },
1424
+ "golang.org/x/oauth2": { name: "x/oauth2", kind: auth("library") },
1425
+ "github.com/redis/go-redis/v9": { name: "Redis (go-redis)", kind: CACHE },
1426
+ "github.com/hibiken/asynq": { name: "Asynq", kind: QUEUE },
1427
+ "github.com/segmentio/kafka-go": { name: "Kafka (kafka-go)", kind: QUEUE },
1428
+ "go.uber.org/zap": { name: "zap logger", kind: OBS },
1429
+ "github.com/rs/zerolog": { name: "zerolog", kind: OBS },
1430
+ "github.com/getsentry/sentry-go": { name: "Sentry", kind: OBS },
1431
+ "github.com/stretchr/testify": { name: "testify", kind: TEST }
1432
+ },
1433
+ cargo: {
1434
+ axum: { name: "Axum", kind: fw("backend") },
1435
+ "actix-web": { name: "Actix Web", kind: fw("backend") },
1436
+ rocket: { name: "Rocket", kind: fw("backend") },
1437
+ warp: { name: "warp", kind: fw("backend") },
1438
+ tonic: { name: "tonic (gRPC)", kind: fw("backend") },
1439
+ leptos: { name: "Leptos", kind: fw("fullstack") },
1440
+ tauri: { name: "Tauri", kind: fw("desktop") },
1441
+ diesel: { name: "Diesel", kind: db("orm") },
1442
+ sqlx: { name: "SQLx", kind: db("driver") },
1443
+ "sea-orm": { name: "SeaORM", kind: db("orm") },
1444
+ "tokio-postgres": { name: "PostgreSQL (tokio-postgres)", kind: db("driver") },
1445
+ mongodb: { name: "MongoDB driver", kind: db("driver") },
1446
+ jsonwebtoken: { name: "jsonwebtoken", kind: auth("token") },
1447
+ argon2: { name: "argon2", kind: auth("hashing") },
1448
+ bcrypt: { name: "bcrypt", kind: auth("hashing") },
1449
+ oauth2: { name: "oauth2", kind: auth("library") },
1450
+ redis: { name: "Redis client", kind: CACHE },
1451
+ moka: { name: "moka cache", kind: CACHE },
1452
+ lapin: { name: "RabbitMQ (lapin)", kind: QUEUE },
1453
+ rdkafka: { name: "Kafka (rdkafka)", kind: QUEUE },
1454
+ tracing: { name: "tracing", kind: OBS },
1455
+ sentry: { name: "Sentry", kind: OBS },
1456
+ "tower-http": { name: "tower-http (may include CORS)", kind: SEC }
1457
+ },
1458
+ maven: {
1459
+ "spring-boot-starter-web": { name: "Spring Boot (Web)", kind: fw("backend") },
1460
+ "spring-boot-starter-webflux": { name: "Spring WebFlux", kind: fw("backend") },
1461
+ "spring-boot-starter": { name: "Spring Boot", kind: fw("backend") },
1462
+ "spring-boot-starter-data-jpa": { name: "Spring Data JPA", kind: db("orm") },
1463
+ "spring-boot-starter-data-mongodb": { name: "Spring Data MongoDB", kind: db("orm") },
1464
+ "hibernate-core": { name: "Hibernate", kind: db("orm") },
1465
+ "mybatis": { name: "MyBatis", kind: db("orm") },
1466
+ postgresql: { name: "PostgreSQL JDBC", kind: db("driver") },
1467
+ "mysql-connector-j": { name: "MySQL JDBC", kind: db("driver") },
1468
+ "flyway-core": { name: "Flyway", kind: db("orm") },
1469
+ "liquibase-core": { name: "Liquibase", kind: db("orm") },
1470
+ "spring-boot-starter-security": { name: "Spring Security", kind: auth("library") },
1471
+ "spring-boot-starter-oauth2-resource-server": { name: "Spring OAuth2 Resource Server", kind: auth("library") },
1472
+ "spring-boot-starter-oauth2-client": { name: "Spring OAuth2 Client", kind: auth("library") },
1473
+ "jjwt-api": { name: "JJWT", kind: auth("token") },
1474
+ "spring-boot-starter-data-redis": { name: "Spring Data Redis", kind: CACHE },
1475
+ "spring-boot-starter-cache": { name: "Spring Cache", kind: CACHE },
1476
+ "spring-kafka": { name: "Spring Kafka", kind: QUEUE },
1477
+ "spring-boot-starter-amqp": { name: "Spring AMQP", kind: QUEUE },
1478
+ "spring-boot-starter-actuator": { name: "Spring Boot Actuator", kind: OBS },
1479
+ "spring-boot-starter-validation": { name: "Bean Validation", kind: SEC },
1480
+ "spring-boot-starter-test": { name: "Spring Boot Test", kind: TEST },
1481
+ "junit-jupiter": { name: "JUnit 5", kind: TEST },
1482
+ junit: { name: "JUnit", kind: TEST },
1483
+ "mockito-core": { name: "Mockito", kind: TEST },
1484
+ testcontainers: { name: "Testcontainers", kind: TEST }
1485
+ },
1486
+ composer: {
1487
+ "laravel/framework": { name: "Laravel", kind: fw("fullstack"), markers: [/(^|\/)artisan$/] },
1488
+ "symfony/framework-bundle": { name: "Symfony", kind: fw("fullstack") },
1489
+ "slim/slim": { name: "Slim", kind: fw("backend") },
1490
+ "doctrine/orm": { name: "Doctrine ORM", kind: db("orm") },
1491
+ "laravel/sanctum": { name: "Laravel Sanctum", kind: auth("library") },
1492
+ "laravel/passport": { name: "Laravel Passport", kind: auth("library") },
1493
+ "tymon/jwt-auth": { name: "jwt-auth", kind: auth("token") },
1494
+ "firebase/php-jwt": { name: "php-jwt", kind: auth("token") },
1495
+ "predis/predis": { name: "Predis (Redis)", kind: CACHE },
1496
+ "sentry/sentry-laravel": { name: "Sentry", kind: OBS },
1497
+ "phpunit/phpunit": { name: "PHPUnit", kind: TEST },
1498
+ "pestphp/pest": { name: "Pest", kind: TEST }
1499
+ },
1500
+ nuget: {
1501
+ "Microsoft.AspNetCore.App": { name: "ASP.NET Core", kind: fw("backend") },
1502
+ "Microsoft.EntityFrameworkCore": { name: "Entity Framework Core", kind: db("orm") },
1503
+ "Npgsql.EntityFrameworkCore.PostgreSQL": { name: "PostgreSQL (Npgsql EF)", kind: db("driver") },
1504
+ "Microsoft.EntityFrameworkCore.SqlServer": { name: "SQL Server (EF)", kind: db("driver") },
1505
+ Dapper: { name: "Dapper", kind: db("orm") },
1506
+ "Microsoft.AspNetCore.Identity.EntityFrameworkCore": { name: "ASP.NET Core Identity", kind: auth("library") },
1507
+ "Microsoft.AspNetCore.Authentication.JwtBearer": { name: "JWT Bearer auth", kind: auth("token") },
1508
+ "StackExchange.Redis": { name: "StackExchange.Redis", kind: CACHE },
1509
+ "MassTransit": { name: "MassTransit", kind: QUEUE },
1510
+ Serilog: { name: "Serilog", kind: OBS },
1511
+ xunit: { name: "xUnit", kind: TEST },
1512
+ NUnit: { name: "NUnit", kind: TEST },
1513
+ MSTest: { name: "MSTest", kind: TEST }
1514
+ },
1515
+ gem: {
1516
+ rails: { name: "Ruby on Rails", kind: fw("fullstack"), markers: [/(^|\/)config\/routes\.rb$/] },
1517
+ sinatra: { name: "Sinatra", kind: fw("backend") },
1518
+ "activerecord": { name: "ActiveRecord", kind: db("orm") },
1519
+ pg: { name: "PostgreSQL (pg gem)", kind: db("driver") },
1520
+ mysql2: { name: "MySQL (mysql2 gem)", kind: db("driver") },
1521
+ devise: { name: "Devise", kind: auth("library") },
1522
+ omniauth: { name: "OmniAuth", kind: auth("library") },
1523
+ pundit: { name: "Pundit (authorization)", kind: auth("library") },
1524
+ cancancan: { name: "CanCanCan (authorization)", kind: auth("library") },
1525
+ jwt: { name: "ruby-jwt", kind: auth("token") },
1526
+ bcrypt: { name: "bcrypt", kind: auth("hashing") },
1527
+ redis: { name: "Redis client", kind: CACHE },
1528
+ sidekiq: { name: "Sidekiq", kind: QUEUE },
1529
+ "sentry-ruby": { name: "Sentry", kind: OBS },
1530
+ brakeman: { name: "Brakeman", kind: SECTOOL },
1531
+ "bundler-audit": { name: "bundler-audit", kind: SECTOOL },
1532
+ rspec: { name: "RSpec", kind: TEST },
1533
+ "rspec-rails": { name: "RSpec Rails", kind: TEST },
1534
+ minitest: { name: "Minitest", kind: TEST }
1535
+ },
1536
+ pub: {
1537
+ flutter: { name: "Flutter", kind: fw("mobile") },
1538
+ firebase_core: { name: "Firebase", kind: db("driver") },
1539
+ cloud_firestore: { name: "Cloud Firestore", kind: db("driver") },
1540
+ sqflite: { name: "SQLite (sqflite)", kind: db("driver") },
1541
+ drift: { name: "Drift", kind: db("orm") },
1542
+ firebase_auth: { name: "Firebase Auth", kind: auth("provider") },
1543
+ supabase_flutter: { name: "Supabase", kind: db("driver") },
1544
+ flutter_test: { name: "flutter_test", kind: TEST },
1545
+ integration_test: { name: "integration_test", kind: TEST },
1546
+ sentry_flutter: { name: "Sentry", kind: OBS }
1547
+ },
1548
+ hex: {
1549
+ phoenix: { name: "Phoenix", kind: fw("fullstack") },
1550
+ ecto_sql: { name: "Ecto", kind: db("orm") },
1551
+ postgrex: { name: "PostgreSQL (Postgrex)", kind: db("driver") },
1552
+ guardian: { name: "Guardian", kind: auth("token") },
1553
+ bcrypt_elixir: { name: "bcrypt_elixir", kind: auth("hashing") },
1554
+ oban: { name: "Oban", kind: QUEUE }
1555
+ }
1556
+ };
1557
+ function ecosystemKey(ecosystem) {
1558
+ const map = {
1559
+ npm: "npm",
1560
+ python: "python",
1561
+ go: "go",
1562
+ cargo: "cargo",
1563
+ maven: "maven",
1564
+ gradle: "maven",
1565
+ composer: "composer",
1566
+ nuget: "nuget",
1567
+ rubygems: "gem",
1568
+ pub: "pub",
1569
+ hex: "hex"
1570
+ };
1571
+ return map[ecosystem] ?? null;
1572
+ }
1573
+ function normalizeDep(ecosystem, dep) {
1574
+ if (ecosystem === "python") return dep.toLowerCase().replace(/_/g, "-");
1575
+ return dep;
1576
+ }
1577
+
1578
+ // src/core/analyzer/detectors/dependencies.ts
1579
+ var dependenciesDetector = {
1580
+ id: "dependencies",
1581
+ version: 1,
1582
+ async run(ctx) {
1583
+ const { model } = ctx;
1584
+ const buckets = {
1585
+ database: /* @__PURE__ */ new Map(),
1586
+ auth: /* @__PURE__ */ new Map(),
1587
+ cache: /* @__PURE__ */ new Map(),
1588
+ queue: /* @__PURE__ */ new Map(),
1589
+ observability: /* @__PURE__ */ new Map(),
1590
+ securityControl: /* @__PURE__ */ new Map(),
1591
+ securityTooling: /* @__PURE__ */ new Map(),
1592
+ testing: /* @__PURE__ */ new Map()
1593
+ };
1594
+ const frameworks = /* @__PURE__ */ new Map();
1595
+ const addEvidence = (prov, ev) => {
1596
+ if (!prov.evidence.some((e) => e.file === ev.file && e.detail === ev.detail)) prov.evidence.push(ev);
1597
+ };
1598
+ for (const manifest of model.manifests) {
1599
+ const eco = ecosystemKey(manifest.ecosystem);
1600
+ if (!eco) continue;
1601
+ const table = CATALOG[eco];
1602
+ const lookup = new Map(Object.entries(table).map(([k, v]) => [normalizeDep(manifest.ecosystem, k), v]));
1603
+ const root = dirOf(manifest.path);
1604
+ const all = [
1605
+ ...manifest.dependencies.map((d) => [d, false]),
1606
+ ...manifest.devDependencies.map((d) => [d, true])
1607
+ ];
1608
+ for (const [dep, isDev] of all) {
1609
+ const entry = lookup.get(normalizeDep(manifest.ecosystem, dep));
1610
+ if (!entry) continue;
1611
+ const ev = { file: manifest.path, detail: `${isDev ? "dev " : ""}dependency "${dep}"` };
1612
+ const marker = entry.markers?.flatMap((re) => ctx.find(re).map((f) => f.path)).find((p) => dirOf(p) === root || p.startsWith(root === "." ? "" : `${root}/`));
1613
+ const kind = entry.kind;
1614
+ const confidence = marker || !isDev || kind.type === "testing" || kind.type === "securityTooling" ? "high" : "medium";
1615
+ const mkProv = () => ({ status: "DETECTED", confidence, source: "config", evidence: [] });
1616
+ if (kind.type === "framework") {
1617
+ const key = `${entry.name}@${root}`;
1618
+ const cur2 = frameworks.get(key) ?? { name: entry.name, category: kind.category, root, provenance: mkProv() };
1619
+ addEvidence(cur2.provenance, ev);
1620
+ if (marker) addEvidence(cur2.provenance, { file: marker, detail: "framework config file" });
1621
+ if (confidence === "high") cur2.provenance.confidence = "high";
1622
+ frameworks.set(key, cur2);
1623
+ continue;
1624
+ }
1625
+ const bucket = buckets[kind.type];
1626
+ const cur = bucket.get(entry.name) ?? { name: entry.name, provenance: mkProv(), ..."kind" in kind ? { kind: kind.kind } : {} };
1627
+ addEvidence(cur.provenance, ev);
1628
+ if (marker) addEvidence(cur.provenance, { file: marker, detail: "config file" });
1629
+ if (confidence === "high") cur.provenance.confidence = "high";
1630
+ bucket.set(entry.name, cur);
1631
+ }
1632
+ }
1633
+ const sorted = (m) => [...m.values()].sort((a, b) => a.name.localeCompare(b.name));
1634
+ model.frameworks = [...frameworks.values()].sort((a, b) => a.root.localeCompare(b.root) || a.name.localeCompare(b.name));
1635
+ model.databases.push(...sorted(buckets.database));
1636
+ model.auth.push(...sorted(buckets.auth));
1637
+ model.caching.push(...sorted(buckets.cache));
1638
+ model.queues.push(...sorted(buckets.queue));
1639
+ model.observability.push(...sorted(buckets.observability));
1640
+ model.security.controls.push(...sorted(buckets.securityControl));
1641
+ model.security.tooling.push(...sorted(buckets.securityTooling));
1642
+ model.tests.frameworks.push(...sorted(buckets.testing));
1643
+ }
1644
+ };
1645
+
1646
+ // src/core/analyzer/detectors/infra.ts
1647
+ import { parseAllDocuments, parse as parseYaml3 } from "yaml";
1648
+ var isObj2 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
1649
+ var IMAGE_CAPS = [
1650
+ [/^(?:.*\/)?(postgres|postgis\/postgis|pgvector\/pgvector|timescale\/timescaledb)(:|$)/, "PostgreSQL", "database"],
1651
+ [/^(?:.*\/)?(mysql|mariadb)(:|$)/, "MySQL/MariaDB", "database"],
1652
+ [/^(?:.*\/)?mongo(:|$)/, "MongoDB", "database"],
1653
+ [/^(?:.*\/)?(mcr\.microsoft\.com\/mssql\/server|mssql)/, "SQL Server", "database"],
1654
+ [/^(?:.*\/)?(elasticsearch|opensearch)/, "Elasticsearch/OpenSearch", "database"],
1655
+ [/^(?:.*\/)?clickhouse/, "ClickHouse", "database"],
1656
+ [/^(?:.*\/)?(redis|valkey|redis\/redis-stack)(:|$)/, "Redis", "cache"],
1657
+ [/^(?:.*\/)?memcached(:|$)/, "Memcached", "cache"],
1658
+ [/^(?:.*\/)?rabbitmq(:|$)/, "RabbitMQ", "queue"],
1659
+ [/^(?:.*\/)?(confluentinc\/cp-kafka|bitnami\/kafka|apache\/kafka)/, "Kafka", "queue"],
1660
+ [/^(?:.*\/)?nats(:|$)/, "NATS", "queue"]
1661
+ ];
1662
+ var HOSTING = [
1663
+ [/(^|\/)vercel\.json$/, "Vercel", "hosting"],
1664
+ [/(^|\/)netlify\.toml$/, "Netlify", "hosting"],
1665
+ [/(^|\/)fly\.toml$/, "Fly.io", "hosting"],
1666
+ [/(^|\/)render\.ya?ml$/, "Render", "hosting"],
1667
+ [/(^|\/)railway\.(json|toml)$/, "Railway", "hosting"],
1668
+ [/(^|\/)Procfile$/, "Procfile (Heroku-style)", "hosting"],
1669
+ [/(^|\/)app\.ya?ml$/, "Google App Engine (app.yaml)", "hosting"],
1670
+ [/(^|\/)firebase\.json$/, "Firebase Hosting/config", "hosting"],
1671
+ [/(^|\/)wrangler\.(toml|jsonc?)$/, "Cloudflare Workers", "hosting"],
1672
+ [/(^|\/)serverless\.ya?ml$/, "Serverless Framework", "hosting"],
1673
+ [/(^|\/)template\.ya?ml$|(^|\/)samconfig\.toml$/, "AWS SAM (possible)", "hosting"],
1674
+ [/(^|\/)cdk\.json$/, "AWS CDK", "iac"],
1675
+ [/(^|\/)Pulumi\.ya?ml$/, "Pulumi", "iac"],
1676
+ [/(^|\/)Chart\.yaml$/, "Helm chart", "kubernetes"],
1677
+ [/(^|\/)kustomization\.ya?ml$/, "Kustomize", "kubernetes"],
1678
+ [/(^|\/)skaffold\.ya?ml$/, "Skaffold", "kubernetes"],
1679
+ [/(^|\/)nginx[^/]*\.conf$|(^|\/)nginx\/.*\.conf$/, "nginx config", "reverse-proxy"],
1680
+ [/(^|\/)Caddyfile$/, "Caddy", "reverse-proxy"],
1681
+ [/(^|\/)traefik\.ya?ml$/, "Traefik", "reverse-proxy"]
1682
+ ];
1683
+ function strList(v) {
1684
+ if (Array.isArray(v)) return v.map((x) => typeof x === "string" || typeof x === "number" ? String(x) : isObj2(x) && x.target ? `${x.published ?? ""}:${x.target}` : "").filter(Boolean);
1685
+ if (isObj2(v)) return Object.keys(v);
1686
+ return [];
1687
+ }
1688
+ function trimCmd(s) {
1689
+ const one = s.split("\n").map((l) => l.trim()).filter(Boolean).join(" && ");
1690
+ return redact(one.length > 160 ? `${one.slice(0, 157)}...` : one);
1691
+ }
1692
+ var COMPOSE_RE = /(^|\/)(docker-)?compose(\.[\w-]+)?\.ya?ml$/;
1693
+ var composeFact = {
1694
+ id: "compose-services",
1695
+ applies: (f) => COMPOSE_RE.test(f.path),
1696
+ compute(text) {
1697
+ const services = [];
1698
+ if (!text) return { services };
1699
+ let y;
1700
+ try {
1701
+ y = parseYaml3(text);
1702
+ } catch {
1703
+ return { invalid: true };
1704
+ }
1705
+ if (!isObj2(y) || !isObj2(y.services)) return { services };
1706
+ for (const [name, svc] of Object.entries(y.services)) {
1707
+ if (!isObj2(svc)) continue;
1708
+ services.push({
1709
+ name,
1710
+ image: typeof svc.image === "string" ? svc.image : isObj2(svc.build) || typeof svc.build === "string" ? "(built locally)" : void 0,
1711
+ ports: strList(svc.ports),
1712
+ dependsOn: strList(svc.depends_on)
1713
+ });
1714
+ }
1715
+ return { services };
1716
+ }
1717
+ };
1718
+ async function composeServices(ctx) {
1719
+ const out = [];
1720
+ for (const f of ctx.find(COMPOSE_RE)) {
1721
+ const v = await ctx.fact(f.path, composeFact);
1722
+ if (!v) continue;
1723
+ if ("invalid" in v) {
1724
+ ctx.warn(`Could not parse ${f.path} (invalid YAML)`);
1725
+ continue;
1726
+ }
1727
+ for (const svc of v.services) out.push({ name: svc.name, image: svc.image, ports: svc.ports, dependsOn: svc.dependsOn, file: f.path });
1728
+ }
1729
+ return out;
1730
+ }
1731
+ function ciYaml(text) {
1732
+ if (!text) return { y: null };
1733
+ try {
1734
+ const y = parseYaml3(text);
1735
+ return { y: isObj2(y) ? y : null };
1736
+ } catch {
1737
+ return { invalid: true };
1738
+ }
1739
+ }
1740
+ var GHA_RE = /^\.github\/workflows\/[^/]+\.ya?ml$/;
1741
+ var githubActionsFact = {
1742
+ id: "ci-github-actions",
1743
+ applies: (f) => GHA_RE.test(f.path),
1744
+ compute(text) {
1745
+ const r = ciYaml(text);
1746
+ if ("invalid" in r) return r;
1747
+ const jobs = [];
1748
+ const y = r.y;
1749
+ if (!y || !isObj2(y.jobs)) return { jobs };
1750
+ for (const [id, job] of Object.entries(y.jobs)) {
1751
+ if (!isObj2(job)) continue;
1752
+ const steps = Array.isArray(job.steps) ? job.steps : [];
1753
+ const commands = steps.flatMap((s) => isObj2(s) && typeof s.run === "string" ? [trimCmd(s.run)] : isObj2(s) && typeof s.uses === "string" ? [`uses: ${s.uses}`] : []);
1754
+ jobs.push({ name: typeof job.name === "string" ? job.name : id, commands: commands.slice(0, 12) });
1755
+ }
1756
+ return { jobs };
1757
+ }
1758
+ };
1759
+ var gitlabFact = {
1760
+ id: "ci-gitlab",
1761
+ applies: (f) => f.path === ".gitlab-ci.yml",
1762
+ compute(text) {
1763
+ const r = ciYaml(text);
1764
+ if ("invalid" in r) return r;
1765
+ const jobs = [];
1766
+ const y = r.y;
1767
+ if (y) for (const [id, job] of Object.entries(y)) {
1768
+ if (id.startsWith(".") || !isObj2(job) || !("script" in job)) continue;
1769
+ const script = Array.isArray(job.script) ? job.script.map(String) : [String(job.script)];
1770
+ jobs.push({ name: id, commands: script.map(trimCmd).slice(0, 12) });
1771
+ }
1772
+ return { jobs };
1773
+ }
1774
+ };
1775
+ var circleFact = {
1776
+ id: "ci-circleci",
1777
+ applies: (f) => f.path === ".circleci/config.yml",
1778
+ compute(text) {
1779
+ const r = ciYaml(text);
1780
+ if ("invalid" in r) return r;
1781
+ const jobs = [];
1782
+ const y = r.y;
1783
+ if (y && isObj2(y.jobs)) for (const [id, job] of Object.entries(y.jobs)) {
1784
+ const steps = isObj2(job) && Array.isArray(job.steps) ? job.steps : [];
1785
+ const commands = steps.flatMap((s) => isObj2(s) && isObj2(s.run) && typeof s.run.command === "string" ? [trimCmd(s.run.command)] : isObj2(s) && typeof s.run === "string" ? [trimCmd(s.run)] : []);
1786
+ jobs.push({ name: id, commands: commands.slice(0, 12) });
1787
+ }
1788
+ return { jobs };
1789
+ }
1790
+ };
1791
+ var JENKINS_RE = /(^|\/)Jenkinsfile$/;
1792
+ var jenkinsFact = {
1793
+ id: "ci-jenkins-stages",
1794
+ applies: (f) => JENKINS_RE.test(f.path),
1795
+ compute: (text) => [...text.matchAll(/stage\s*\(\s*['"]([^'"]+)['"]/g)].map((m) => m[1])
1796
+ };
1797
+ async function ciJobs(ctx) {
1798
+ const jobs = [];
1799
+ const load = async (p, def) => {
1800
+ const v = await ctx.fact(p, def);
1801
+ if (!v) return [];
1802
+ if ("invalid" in v) {
1803
+ ctx.warn(`Could not parse ${p} (invalid YAML)`);
1804
+ return [];
1805
+ }
1806
+ return v.jobs;
1807
+ };
1808
+ for (const f of ctx.find(GHA_RE)) {
1809
+ for (const j of await load(f.path, githubActionsFact)) jobs.push({ system: "GitHub Actions", file: f.path, name: j.name, commands: j.commands });
1810
+ }
1811
+ if (ctx.has(".gitlab-ci.yml")) {
1812
+ for (const j of await load(".gitlab-ci.yml", gitlabFact)) jobs.push({ system: "GitLab CI", file: ".gitlab-ci.yml", name: j.name, commands: j.commands });
1813
+ }
1814
+ if (ctx.has(".circleci/config.yml")) {
1815
+ for (const j of await load(".circleci/config.yml", circleFact)) jobs.push({ system: "CircleCI", file: ".circleci/config.yml", name: j.name, commands: j.commands });
1816
+ }
1817
+ for (const f of ctx.find(/(^|\/)azure-pipelines\.ya?ml$/)) jobs.push({ system: "Azure Pipelines", file: f.path, name: "pipeline", commands: [] });
1818
+ for (const f of ctx.find(/(^|\/)bitbucket-pipelines\.yml$/)) jobs.push({ system: "Bitbucket Pipelines", file: f.path, name: "pipeline", commands: [] });
1819
+ for (const f of ctx.find(JENKINS_RE)) {
1820
+ const stages = await ctx.fact(f.path, jenkinsFact) ?? [];
1821
+ if (stages.length) for (const s of stages) jobs.push({ system: "Jenkins", file: f.path, name: s, commands: [] });
1822
+ else jobs.push({ system: "Jenkins", file: f.path, name: "pipeline", commands: [] });
1823
+ }
1824
+ return jobs;
1825
+ }
1826
+ var DOCKERFILE_RE = /(^|\/)(Dockerfile|Containerfile)(\.[\w.-]+)?$|\.dockerfile$/i;
1827
+ var dockerfileFact = {
1828
+ id: "dockerfile",
1829
+ applies: (f) => DOCKERFILE_RE.test(f.path),
1830
+ compute(text) {
1831
+ if (!text) return false;
1832
+ const baseImages = [...text.matchAll(/^\s*FROM\s+(?:--platform=\S+\s+)?(\S+)/gim)].map((m) => m[1]);
1833
+ const exposedPorts = [...text.matchAll(/^\s*EXPOSE\s+(.+)$/gim)].flatMap((m) => m[1].trim().split(/\s+/));
1834
+ return { baseImages: [...new Set(baseImages)], exposedPorts: [...new Set(exposedPorts)] };
1835
+ }
1836
+ };
1837
+ var terraformFact = {
1838
+ id: "terraform-providers",
1839
+ applies: (f) => /\.tf$/.test(f.path),
1840
+ compute(text) {
1841
+ const out = [];
1842
+ if (!text) return out;
1843
+ for (const m of text.matchAll(/(?:provider|required_providers)\s*"?([a-z0-9_-]+)"?\s*\{|source\s*=\s*"(?:[\w.-]+\/)?([\w-]+)\/([\w-]+)"/g)) {
1844
+ const p = m[1] ?? m[3];
1845
+ if (p && p !== "required_providers") out.push(p);
1846
+ }
1847
+ return out;
1848
+ }
1849
+ };
1850
+ var K8S_SKIP = /(^|\/)(\.github|\.gitlab|\.circleci)\//;
1851
+ var k8sFact = {
1852
+ id: "k8s-kinds",
1853
+ applies: (f) => /\.ya?ml$/.test(f.path) && !f.large && !K8S_SKIP.test(f.path) && !/compose/.test(f.path),
1854
+ compute(text) {
1855
+ const kinds = [];
1856
+ if (!text || !/^apiVersion:/m.test(text) || !/^kind:/m.test(text)) return kinds;
1857
+ try {
1858
+ for (const doc of parseAllDocuments(text)) {
1859
+ const j = doc.toJSON();
1860
+ if (isObj2(j) && typeof j.kind === "string" && typeof j.apiVersion === "string") kinds.push(j.kind);
1861
+ }
1862
+ } catch {
1863
+ }
1864
+ return kinds;
1865
+ }
1866
+ };
1867
+ var infraDetector = {
1868
+ id: "infra",
1869
+ version: 1,
1870
+ facts: [dockerfileFact, composeFact, terraformFact, k8sFact, githubActionsFact, gitlabFact, circleFact, jenkinsFact],
1871
+ async run(ctx) {
1872
+ const { model } = ctx;
1873
+ for (const f of ctx.find(DOCKERFILE_RE)) {
1874
+ const v = await ctx.fact(f.path, dockerfileFact);
1875
+ if (!v) continue;
1876
+ model.containers.dockerfiles.push({ path: f.path, baseImages: v.baseImages, exposedPorts: v.exposedPorts });
1877
+ }
1878
+ model.containers.services = await composeServices(ctx);
1879
+ for (const svc of model.containers.services) {
1880
+ if (!svc.image) continue;
1881
+ for (const [re, name, cap] of IMAGE_CAPS) {
1882
+ if (!re.test(svc.image)) continue;
1883
+ const prov = detected("config", [{ file: svc.file, detail: `compose service "${svc.name}" image ${svc.image}` }]);
1884
+ if (cap === "database") model.databases.push({ name, kind: "service", provenance: prov });
1885
+ else if (cap === "cache") model.caching.push({ name: `${name} (container)`, provenance: prov });
1886
+ else model.queues.push({ name: `${name} (container)`, provenance: prov });
1887
+ }
1888
+ }
1889
+ for (const [re, name, kind] of HOSTING) {
1890
+ for (const f of ctx.find(re)) model.infrastructure.push({ name, kind, file: f.path, provenance: detected("filesystem", [{ file: f.path }], name.includes("possible") ? "low" : "high") });
1891
+ }
1892
+ const tfProviders = /* @__PURE__ */ new Map();
1893
+ for (const f of ctx.find(/\.tf$/)) {
1894
+ const providers = await ctx.fact(f.path, terraformFact);
1895
+ if (!providers) continue;
1896
+ for (const p of providers) if (!tfProviders.has(p)) tfProviders.set(p, f.path);
1897
+ }
1898
+ for (const [p, file] of tfProviders) model.infrastructure.push({ name: `Terraform provider: ${p}`, kind: "iac", file, provenance: detected("config", [{ file }]) });
1899
+ const k8sKinds = /* @__PURE__ */ new Map();
1900
+ for (const f of ctx.find(/\.ya?ml$/)) {
1901
+ if (f.large || K8S_SKIP.test(f.path) || /compose/.test(f.path)) continue;
1902
+ const kinds = await ctx.fact(f.path, k8sFact);
1903
+ if (!kinds) continue;
1904
+ for (const kind of kinds) if (!k8sKinds.has(kind)) k8sKinds.set(kind, f.path);
1905
+ }
1906
+ for (const [kind, file] of k8sKinds) model.infrastructure.push({ name: `Kubernetes ${kind}`, kind: "kubernetes", file, provenance: detected("config", [{ file }]) });
1907
+ model.ci = await ciJobs(ctx);
1908
+ for (const f of ctx.find(/(^|\/)(\.github\/dependabot\.ya?ml|renovate\.json5?|\.renovaterc(\.json)?|\.snyk|\.semgrep\.ya?ml|\.gitleaks\.toml|\.trivyignore|codeql[^/]*\.ya?ml|\.pre-commit-config\.yaml|SECURITY\.md)$/)) {
1909
+ const name = /dependabot/.test(f.path) ? "Dependabot" : /renovate/.test(f.path) ? "Renovate" : /snyk/.test(f.path) ? "Snyk" : /semgrep/.test(f.path) ? "Semgrep" : /gitleaks/.test(f.path) ? "Gitleaks" : /trivy/.test(f.path) ? "Trivy" : /codeql/.test(f.path) ? "CodeQL" : /pre-commit/.test(f.path) ? "pre-commit hooks" : "Security policy (SECURITY.md)";
1910
+ model.security.tooling.push({ name, provenance: detected("filesystem", [{ file: f.path }]) });
1911
+ }
1912
+ for (const job of model.ci) {
1913
+ for (const c of job.commands) {
1914
+ const m = /(codeql|trivy|snyk|semgrep|gitleaks|npm audit|pip-audit|govulncheck|cargo audit|bandit|brakeman|zap)/i.exec(c);
1915
+ if (m) model.security.tooling.push({ name: `${m[1]} (in CI)`, provenance: detected("config", [{ file: job.file, detail: `job "${job.name}"` }]) });
1916
+ }
1917
+ }
1918
+ }
1919
+ };
1920
+
1921
+ // src/core/analyzer/detectors/tests.ts
1922
+ var TEST_FILE_RE = TEST_FILE;
1923
+ var SOURCE_RE = SOURCE_FILE;
1924
+ var RUNNER_CONFIGS = [
1925
+ [/(^|\/)jest\.config\.(js|ts|cjs|mjs|json)$/, "Jest"],
1926
+ [/(^|\/)vitest\.config\.(js|ts|mjs|cjs)$/, "Vitest"],
1927
+ [/(^|\/)playwright\.config\.(js|ts)$/, "Playwright"],
1928
+ [/(^|\/)cypress\.config\.(js|ts)$|(^|\/)cypress\.json$/, "Cypress"],
1929
+ [/(^|\/)(pytest\.ini|conftest\.py)$/, "pytest"],
1930
+ [/(^|\/)\.mocharc\.(js|json|ya?ml|cjs)$/, "Mocha"],
1931
+ [/(^|\/)karma\.conf\.(js|ts)$/, "Karma"],
1932
+ [/(^|\/)phpunit\.xml(\.dist)?$/, "PHPUnit"],
1933
+ [/(^|\/)\.rspec$/, "RSpec"]
1934
+ ];
1935
+ var COVERAGE = /(^|\/)(\.nycrc(\.json)?|\.coveragerc|codecov\.ya?ml|\.codecov\.ya?ml|jacoco[^/]*\.xml|\.c8rc(\.json)?|coverage\.xml|sonar-project\.properties)$/;
1936
+ var COVERAGE_CONFIG = /(^|\/)(jest|vitest)\.config\.(js|ts|mjs|cjs)$/;
1937
+ var rustTestFact = {
1938
+ id: "rust-test-attr",
1939
+ applies: (f) => /\.rs$/.test(f.path),
1940
+ compute: (text) => !!text && /#\[(tokio::)?test\]/.test(text)
1941
+ };
1942
+ var coverageFact = {
1943
+ id: "runner-coverage",
1944
+ applies: (f) => COVERAGE_CONFIG.test(f.path),
1945
+ compute: (text) => !!text && /coverage/.test(text)
1946
+ };
1947
+ var testsDetector = {
1948
+ id: "tests",
1949
+ version: 1,
1950
+ facts: [rustTestFact, coverageFact],
1951
+ async run(ctx) {
1952
+ const { model } = ctx;
1953
+ const testFiles = ctx.find((f) => !f.binary && SOURCE_RE.test(f.path) && TEST_FILE_RE.test(f.path));
1954
+ model.tests.testFileCount = testFiles.length;
1955
+ const dirs = /* @__PURE__ */ new Map();
1956
+ for (const f of testFiles) {
1957
+ const m = /^(.*?(?:__tests__|tests?|spec|specs|e2e))\//.exec(f.path);
1958
+ if (m) dirs.set(m[1], (dirs.get(m[1]) ?? 0) + 1);
1959
+ }
1960
+ model.tests.testDirs = [...dirs.entries()].sort((a, b) => b[1] - a[1]).slice(0, 15).map(([d]) => d);
1961
+ const known = new Set(model.tests.frameworks.map((f) => f.name));
1962
+ for (const [re, name] of RUNNER_CONFIGS) {
1963
+ const files = ctx.find(re);
1964
+ if (!files.length) continue;
1965
+ const existing = model.tests.frameworks.find((f) => f.name === name);
1966
+ if (existing) existing.provenance.evidence.push(...files.slice(0, 3).map((f) => ({ file: f.path, detail: "runner config" })));
1967
+ else if (!known.has(name)) model.tests.frameworks.push({ name, provenance: detected("config", files.slice(0, 3).map((f) => ({ file: f.path, detail: "runner config" }))) });
1968
+ }
1969
+ if (ctx.find(/_test\.go$/).length && !known.has("go test")) model.tests.frameworks.push({ name: "go test", provenance: detected("filesystem", ctx.find(/_test\.go$/).slice(0, 3).map((f) => ({ file: f.path }))) });
1970
+ if (model.manifests.some((m) => m.ecosystem === "cargo") && ctx.find(/\.rs$/).length) {
1971
+ const withTests = [];
1972
+ for (const f of ctx.find(/\.rs$/).slice(0, 2e3)) {
1973
+ if (await ctx.fact(f.path, rustTestFact)) withTests.push(f.path);
1974
+ if (withTests.length >= 3) break;
1975
+ }
1976
+ if (withTests.length) model.tests.frameworks.push({ name: "cargo test", provenance: detected("code", withTests.map((file) => ({ file, detail: "#[test] attribute" }))) });
1977
+ }
1978
+ model.tests.frameworks.sort((a, b) => a.name.localeCompare(b.name));
1979
+ model.tests.coverageConfigs = ctx.find(COVERAGE).map((f) => f.path);
1980
+ for (const f of ctx.find(COVERAGE_CONFIG)) {
1981
+ if (await ctx.fact(f.path, coverageFact)) model.tests.coverageConfigs.push(f.path);
1982
+ }
1983
+ if (model.workspace.isMonorepo) {
1984
+ for (const pkg of model.workspace.packages) {
1985
+ if (pkg.path === ".") continue;
1986
+ const prefix = `${pkg.path}/`;
1987
+ const src = ctx.files.some((f) => f.path.startsWith(prefix) && SOURCE_RE.test(f.path) && !TEST_FILE_RE.test(f.path));
1988
+ const tests = testFiles.some((f) => f.path.startsWith(prefix));
1989
+ if (src && !tests) model.tests.packagesWithoutTests.push(pkg.path);
1990
+ }
1991
+ }
1992
+ }
1993
+ };
1994
+
1995
+ // src/core/analyzer/detectors/database.ts
1996
+ var prismaFact = {
1997
+ id: "db-prisma",
1998
+ applies: (f) => /\.prisma$/.test(f.path),
1999
+ compute(text) {
2000
+ if (!text) return false;
2001
+ const provider = /datasource\s+\w+\s*\{[^}]*provider\s*=\s*"([^"]+)"/.exec(text)?.[1] ?? null;
2002
+ const models = [];
2003
+ for (const m of text.matchAll(/^(model|view)\s+(\w+)\s*\{([\s\S]*?)^\}/gm)) {
2004
+ const body = m[3];
2005
+ const fields = [];
2006
+ const indexes = [];
2007
+ const relations = [];
2008
+ for (const raw of body.split("\n")) {
2009
+ const line = raw.replace(/\/\/.*$/, "").trim();
2010
+ if (!line) continue;
2011
+ if (line.startsWith("@@")) {
2012
+ indexes.push(line);
2013
+ continue;
2014
+ }
2015
+ const fm = /^(\w+)\s+([\w[\]?.()"]+)\s*(.*)$/.exec(line);
2016
+ if (!fm) continue;
2017
+ const attributes = [...fm[3].matchAll(/@[\w.]+(\([^)]*\))?/g)].map((a) => a[0]);
2018
+ fields.push({ name: fm[1], type: fm[2], attributes });
2019
+ const rel = attributes.find((a) => a.startsWith("@relation"));
2020
+ if (rel) relations.push(`${fm[1]} \u2192 ${fm[2].replace(/[[\]?]/g, "")}`);
2021
+ }
2022
+ models.push({ name: m[2], kind: "model", fields, indexes, relations, line: text.slice(0, m.index).split("\n").length });
2023
+ }
2024
+ return { provider, models };
2025
+ }
2026
+ };
2027
+ async function prisma(ctx) {
2028
+ const out = [];
2029
+ for (const f of ctx.find(/\.prisma$/)) {
2030
+ const v = await ctx.fact(f.path, prismaFact);
2031
+ if (!v) continue;
2032
+ if (v.provider) ctx.model.databases.push({ name: `${v.provider} (Prisma datasource)`, kind: "engine", provenance: detected("config", [{ file: f.path, detail: "datasource provider" }]) });
2033
+ for (const e of v.models) out.push({ name: e.name, kind: e.kind, fields: e.fields, indexes: e.indexes, relations: e.relations, file: f.path, provenance: detected("code", [{ file: f.path, line: e.line }]) });
2034
+ }
2035
+ return out;
2036
+ }
2037
+ function stripSqlComments(sql) {
2038
+ return sql.replace(/--[^\n]*/g, "").replace(/\/\*[\s\S]*?\*\//g, "");
2039
+ }
2040
+ var SQL_RE = /\.sql$/i;
2041
+ var sqlFact = {
2042
+ id: "db-sql",
2043
+ applies: (f) => SQL_RE.test(f.path) && !f.large,
2044
+ compute(raw) {
2045
+ if (!raw) return false;
2046
+ const text = stripSqlComments(raw);
2047
+ const out = { tables: [], indexes: [] };
2048
+ for (const m of text.matchAll(/CREATE\s+(?:UNLOGGED\s+|TEMP(?:ORARY)?\s+)?TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?([`"[\]\w.]+)\s*\(([\s\S]*?)\)\s*;/gi)) {
2049
+ const name = m[1].replace(/[`"[\]]/g, "");
2050
+ const fields = [];
2051
+ const relations = [];
2052
+ for (const col of splitTopLevel(m[2])) {
2053
+ const c = col.trim();
2054
+ const fk = /FOREIGN\s+KEY\s*\(([^)]+)\)\s*REFERENCES\s+([`"\w.]+)/i.exec(c);
2055
+ if (fk) {
2056
+ relations.push(`${fk[1].trim()} \u2192 ${fk[2].replace(/[`"]/g, "")}`);
2057
+ continue;
2058
+ }
2059
+ if (/^(CONSTRAINT|PRIMARY\s+KEY|UNIQUE|CHECK|INDEX|KEY)\b/i.test(c)) continue;
2060
+ const cm = /^([`"\w]+)\s+([\w]+(?:\s*\([^)]*\))?)(.*)$/s.exec(c);
2061
+ if (!cm) continue;
2062
+ const attrs = [];
2063
+ if (/PRIMARY\s+KEY/i.test(cm[3])) attrs.push("PRIMARY KEY");
2064
+ if (/NOT\s+NULL/i.test(cm[3])) attrs.push("NOT NULL");
2065
+ if (/\bUNIQUE\b/i.test(cm[3])) attrs.push("UNIQUE");
2066
+ const ref = /REFERENCES\s+([`"\w.]+)/i.exec(cm[3]);
2067
+ if (ref) relations.push(`${cm[1].replace(/[`"]/g, "")} \u2192 ${ref[1].replace(/[`"]/g, "")}`);
2068
+ fields.push({ name: cm[1].replace(/[`"]/g, ""), type: cm[2], attributes: attrs });
2069
+ }
2070
+ const line = raw.slice(0, raw.indexOf(m[0].slice(0, 20))).split("\n").length;
2071
+ out.tables.push({ name, fields, relations, line: line > 0 ? line : void 0 });
2072
+ }
2073
+ for (const m of text.matchAll(/CREATE\s+(UNIQUE\s+)?INDEX\s+(?:CONCURRENTLY\s+)?(?:IF\s+NOT\s+EXISTS\s+)?([`"\w.]+)\s+ON\s+([`"\w.]+)\s*(?:USING\s+\w+\s*)?\(([^)]*)\)/gi)) {
2074
+ out.indexes.push([m[3].replace(/[`"]/g, "").toLowerCase(), `${m[1] ? "UNIQUE " : ""}${m[2].replace(/[`"]/g, "")} (${m[4].trim()})`]);
2075
+ }
2076
+ return out;
2077
+ }
2078
+ };
2079
+ async function sqlFiles(ctx) {
2080
+ const tables = /* @__PURE__ */ new Map();
2081
+ const files = ctx.find((f) => SQL_RE.test(f.path) && !f.large).slice(0, 2e3);
2082
+ for (const f of files) {
2083
+ const v = await ctx.fact(f.path, sqlFact);
2084
+ if (!v) continue;
2085
+ for (const t of v.tables) {
2086
+ tables.set(t.name.toLowerCase(), { name: t.name, kind: "table", fields: t.fields, indexes: tables.get(t.name.toLowerCase())?.indexes ?? [], relations: t.relations, file: f.path, provenance: detected("code", [{ file: f.path, line: t.line }]) });
2087
+ }
2088
+ for (const [table, index] of v.indexes) {
2089
+ const t = tables.get(table);
2090
+ if (t) t.indexes.push(index);
2091
+ }
2092
+ }
2093
+ return [...tables.values()];
2094
+ }
2095
+ function splitTopLevel(s) {
2096
+ const parts = [];
2097
+ let depth = 0;
2098
+ let cur = "";
2099
+ for (const ch of s) {
2100
+ if (ch === "(") depth++;
2101
+ if (ch === ")") depth--;
2102
+ if (ch === "," && depth === 0) {
2103
+ parts.push(cur);
2104
+ cur = "";
2105
+ } else cur += ch;
2106
+ }
2107
+ if (cur.trim()) parts.push(cur);
2108
+ return parts;
2109
+ }
2110
+ var DJANGO_MODELS_RE = /(^|\/)models(\.py|\/[^/]+\.py)$/;
2111
+ var djangoFact = {
2112
+ id: "db-django-models",
2113
+ applies: (f) => DJANGO_MODELS_RE.test(f.path),
2114
+ compute(text) {
2115
+ const out = [];
2116
+ if (!text || !/from django\.db import models|models\.Model/.test(text)) return out;
2117
+ const classRe = /^class\s+(\w+)\(([^)]*models\.Model[^)]*)\):\s*\n((?:[ \t]+.*\n|\s*\n)*)/gm;
2118
+ for (const m of text.matchAll(classRe)) {
2119
+ const fields = [];
2120
+ const relations = [];
2121
+ for (const fm of m[3].matchAll(/^\s+(\w+)\s*=\s*models\.(\w+)\(([^)\n]*)/gm)) {
2122
+ fields.push({ name: fm[1], type: fm[2], attributes: [] });
2123
+ if (/ForeignKey|OneToOneField|ManyToManyField/.test(fm[2])) relations.push(`${fm[1]} \u2192 ${fm[3].split(",")[0].trim().replace(/["']/g, "")}`);
2124
+ }
2125
+ out.push({ name: m[1], kind: "model", fields, indexes: [], relations, line: text.slice(0, m.index).split("\n").length });
2126
+ }
2127
+ return out;
2128
+ }
2129
+ };
2130
+ var entitiesFrom = (file, list) => (list ?? []).map((e) => ({ name: e.name, kind: e.kind, fields: e.fields, indexes: e.indexes, relations: e.relations, file, provenance: detected("code", [{ file, line: e.line }], "medium") }));
2131
+ async function djangoModels(ctx) {
2132
+ const out = [];
2133
+ for (const f of ctx.find(DJANGO_MODELS_RE)) out.push(...entitiesFrom(f.path, await ctx.fact(f.path, djangoFact)));
2134
+ return out;
2135
+ }
2136
+ var sqlalchemyFact = {
2137
+ id: "db-sqlalchemy-models",
2138
+ applies: (f) => /\.py$/.test(f.path),
2139
+ compute(text) {
2140
+ const out = [];
2141
+ if (!text || !/__tablename__|table=True/.test(text)) return out;
2142
+ for (const m of text.matchAll(/^class\s+(\w+)\(([^)]*)\):\s*\n((?:[ \t]+.*\n|\s*\n)*)/gm)) {
2143
+ const body = m[3];
2144
+ const tn = /__tablename__\s*=\s*["'](\w+)["']/.exec(body)?.[1];
2145
+ if (!tn && !/table\s*=\s*True/.test(m[2])) continue;
2146
+ const fields = [...body.matchAll(/^\s+(\w+)\s*(?::\s*[^=\n]+)?=\s*(?:Column|mapped_column|Field)\(/gm)].map((fm) => ({ name: fm[1], attributes: [] }));
2147
+ out.push({ name: tn ?? m[1], kind: "table", fields, indexes: [], relations: [], line: text.slice(0, m.index).split("\n").length });
2148
+ }
2149
+ return out;
2150
+ }
2151
+ };
2152
+ async function sqlalchemyModels(ctx) {
2153
+ const out = [];
2154
+ if (!ctx.model.databases.some((d) => /SQLAlchemy|SQLModel/.test(d.name))) return out;
2155
+ for (const f of ctx.find(/\.py$/).slice(0, 5e3)) out.push(...entitiesFrom(f.path, await ctx.fact(f.path, sqlalchemyFact)));
2156
+ return out;
2157
+ }
2158
+ var MONGOOSE_RE = /\.(m|c)?(t|j)s$/;
2159
+ var mongooseFact = {
2160
+ id: "db-mongoose-models",
2161
+ applies: (f) => MONGOOSE_RE.test(f.path),
2162
+ compute(text) {
2163
+ const out = [];
2164
+ if (!text || !text.includes("model(")) return out;
2165
+ for (const m of text.matchAll(/mongoose\.model\s*(?:<[^>]*>)?\(\s*["'](\w+)["']|\bmodel\s*(?:<[^>]*>)?\(\s*["'](\w+)["']\s*,/g)) {
2166
+ out.push([m[1] ?? m[2], text.slice(0, m.index).split("\n").length]);
2167
+ }
2168
+ return out;
2169
+ }
2170
+ };
2171
+ async function mongooseModels(ctx) {
2172
+ if (!ctx.model.databases.some((d) => d.name === "Mongoose")) return [];
2173
+ const out = [];
2174
+ for (const f of ctx.find(MONGOOSE_RE).slice(0, 5e3)) {
2175
+ for (const [name, line] of await ctx.fact(f.path, mongooseFact) ?? []) {
2176
+ out.push({ name, kind: "collection", fields: [], indexes: [], relations: [], file: f.path, provenance: detected("code", [{ file: f.path, line }], "medium") });
2177
+ }
2178
+ }
2179
+ return out;
2180
+ }
2181
+ var MIGRATION_DIRS = [
2182
+ [/(^|\/)prisma\/migrations\/[^/]+\/migration\.sql$/, "Prisma Migrate"],
2183
+ [/(^|\/)drizzle\/[^/]*\.sql$/, "Drizzle Kit"],
2184
+ [/(^|\/)alembic\/versions\/[^/]+\.py$|(^|\/)migrations\/versions\/[^/]+\.py$/, "Alembic"],
2185
+ [/(^|\/)migrations\/\d{4}_[^/]+\.py$/, "Django migrations"],
2186
+ [/(^|\/)db\/migrate\/[^/]+\.rb$/, "Rails migrations"],
2187
+ [/(^|\/)db\/migration\/V\d+[^/]*\.sql$|(^|\/)resources\/db\/migration\/[^/]+\.sql$/, "Flyway"],
2188
+ [/(^|\/)(db\/)?changelog\/[^/]+\.(xml|ya?ml|sql)$/, "Liquibase"],
2189
+ [/(^|\/)database\/migrations\/[^/]+\.php$/, "Laravel migrations"],
2190
+ [/(^|\/)Migrations\/\d+_[^/]+\.cs$/, "EF Core migrations"],
2191
+ [/(^|\/)migrations\/[^/]+\.(up|down)\.sql$|(^|\/)migrations\/\d+[^/]*\.sql$/, "SQL migrations"],
2192
+ [/(^|\/)priv\/repo\/migrations\/[^/]+\.exs$/, "Ecto migrations"],
2193
+ [/(^|\/)supabase\/migrations\/[^/]+\.sql$/, "Supabase migrations"]
2194
+ ];
2195
+ var databaseDetector = {
2196
+ id: "database",
2197
+ version: 1,
2198
+ facts: [prismaFact, sqlFact, djangoFact, sqlalchemyFact, mongooseFact],
2199
+ async run(ctx) {
2200
+ const { model } = ctx;
2201
+ const entities = [...await prisma(ctx), ...await sqlFiles(ctx), ...await djangoModels(ctx), ...await sqlalchemyModels(ctx), ...await mongooseModels(ctx)];
2202
+ model.dbEntities = entities.sort((a, b) => a.name.localeCompare(b.name));
2203
+ const seen = /* @__PURE__ */ new Set();
2204
+ for (const [re, tool] of MIGRATION_DIRS) {
2205
+ const files = ctx.find(re).filter((f) => !seen.has(f.path));
2206
+ if (!files.length) continue;
2207
+ files.forEach((f) => seen.add(f.path));
2208
+ const byDir = /* @__PURE__ */ new Map();
2209
+ for (const f of files) {
2210
+ const d = tool === "Prisma Migrate" ? dirOf(dirOf(f.path)) : dirOf(f.path);
2211
+ byDir.set(d, (byDir.get(d) ?? 0) + 1);
2212
+ }
2213
+ for (const [dir, count] of byDir) model.migrations.push({ path: dir, tool, count });
2214
+ }
2215
+ const merged = /* @__PURE__ */ new Map();
2216
+ for (const d of model.databases) {
2217
+ const cur = merged.get(d.name);
2218
+ if (cur) cur.provenance.evidence.push(...d.provenance.evidence);
2219
+ else merged.set(d.name, d);
2220
+ }
2221
+ model.databases = [...merged.values()];
2222
+ }
2223
+ };
2224
+
2225
+ // src/core/analyzer/detectors/routes.ts
2226
+ import { parse as babelParse } from "@babel/parser";
2227
+ var HTTP = ["get", "post", "put", "patch", "delete", "options", "head", "all"];
2228
+ var MAX_ROUTES = 5e3;
2229
+ var isNode = (v) => typeof v === "object" && v !== null && typeof v.type === "string";
2230
+ function walk(node, visit, parent = null) {
2231
+ if (Array.isArray(node)) {
2232
+ for (const c of node) walk(c, visit, parent);
2233
+ return;
2234
+ }
2235
+ if (!isNode(node)) return;
2236
+ visit(node, parent);
2237
+ for (const key of Object.keys(node)) {
2238
+ if (key === "loc" || key === "start" || key === "end" || key === "extra" || key === "comments" || key === "leadingComments" || key === "trailingComments") continue;
2239
+ const v = node[key];
2240
+ if (typeof v === "object" && v !== null) walk(v, visit, node);
2241
+ }
2242
+ }
2243
+ function strValue(n) {
2244
+ if (!isNode(n)) return null;
2245
+ if (n.type === "StringLiteral") return n.value;
2246
+ if (n.type === "TemplateLiteral" && Array.isArray(n.quasis) && n.quasis.length === 1) return n.quasis[0].value.cooked;
2247
+ return null;
2248
+ }
2249
+ var lineOf = (n) => n.loc?.start?.line;
2250
+ function jsRoutes(file, text, framework) {
2251
+ let ast;
2252
+ try {
2253
+ ast = babelParse(text, { sourceType: "unambiguous", errorRecovery: true, plugins: ["typescript", "jsx", "decorators"] });
2254
+ } catch {
2255
+ return [];
2256
+ }
2257
+ const routes = [];
2258
+ const prov = (line) => detected("code", [{ file, line }], "high");
2259
+ walk(ast, (n) => {
2260
+ if (n.type === "CallExpression" && isNode(n.callee) && n.callee.type === "MemberExpression" && isNode(n.callee.property) && n.callee.property.type === "Identifier") {
2261
+ const method = n.callee.property.name.toLowerCase();
2262
+ const args = n.arguments;
2263
+ if (HTTP.includes(method) && args.length >= 2) {
2264
+ const p = strValue(args[0]);
2265
+ if (p !== null && (p.startsWith("/") || p === "*")) routes.push({ method: method.toUpperCase(), path: p, framework, file, line: lineOf(n), provenance: prov(lineOf(n)) });
2266
+ }
2267
+ if (method === "route" && isNode(args[0]) && args[0].type === "ObjectExpression") {
2268
+ let m = null;
2269
+ let u = null;
2270
+ for (const prop of args[0].properties) {
2271
+ const k = isNode(prop.key) ? prop.key.name ?? prop.key.value : null;
2272
+ if (k === "method") m = strValue(prop.value);
2273
+ if (k === "url" || k === "path") u = strValue(prop.value);
2274
+ }
2275
+ if (m && u) routes.push({ method: m.toUpperCase(), path: u, framework, file, line: lineOf(n), provenance: prov(lineOf(n)) });
2276
+ }
2277
+ }
2278
+ if ((n.type === "ClassDeclaration" || n.type === "ClassExpression") && Array.isArray(n.decorators)) {
2279
+ let prefix = null;
2280
+ for (const d of n.decorators) {
2281
+ const expr = d.expression;
2282
+ if (expr?.type === "CallExpression" && isNode(expr.callee) && expr.callee.name === "Controller") {
2283
+ const a = expr.arguments[0];
2284
+ prefix = strValue(a) ?? (isNode(a) && a.type === "ObjectExpression" ? null : "");
2285
+ }
2286
+ }
2287
+ if (prefix === null) return;
2288
+ const body = n.body.body;
2289
+ for (const member of body) {
2290
+ for (const d of member.decorators ?? []) {
2291
+ const expr = d.expression;
2292
+ if (expr?.type !== "CallExpression" || !isNode(expr.callee)) continue;
2293
+ const name = expr.callee.name;
2294
+ if (!["Get", "Post", "Put", "Patch", "Delete", "Options", "Head", "All"].includes(name)) continue;
2295
+ const sub = strValue(expr.arguments[0]) ?? "";
2296
+ const full = `/${[prefix, sub].filter(Boolean).join("/")}`.replace(/\/+/g, "/");
2297
+ routes.push({ method: name.toUpperCase(), path: full, framework: "NestJS", file, line: lineOf(d), provenance: prov(lineOf(d)) });
2298
+ }
2299
+ }
2300
+ }
2301
+ });
2302
+ return routes;
2303
+ }
2304
+ function regexRoutes(text, re, pick) {
2305
+ const out = [];
2306
+ for (const m of text.matchAll(re)) {
2307
+ const r = pick(m);
2308
+ if (!r) continue;
2309
+ const line = text.slice(0, m.index).split("\n").length;
2310
+ out.push([r.method, r.path, line]);
2311
+ }
2312
+ return out;
2313
+ }
2314
+ var toRoutes = (file, framework, rows) => (rows ?? []).map(([method, path7, line]) => ({ method, path: path7, framework, file, line: line ?? void 0, provenance: detected("code", [{ file, line: line ?? void 0 }], "medium") }));
2315
+ var JS_RE = /\.(m|c)?(t|j)sx?$/;
2316
+ var JS_FRAMEWORKS = ["Express", "Fastify", "Koa", "Hono", "NestJS"];
2317
+ var jsRoutesFact = {
2318
+ id: "routes-js",
2319
+ applies: (f) => JS_RE.test(f.path) && !f.large && !/\.(test|spec|d)\.[cm]?[jt]sx?$/.test(f.path),
2320
+ when: (hints) => JS_FRAMEWORKS.some((n) => hints.frameworks.has(n)),
2321
+ compute(text, rel) {
2322
+ if (!text || !/\b(express|fastify|hono|koa|@nestjs\/common|Router)\b/.test(text)) return false;
2323
+ const guess = /@nestjs\/common/.test(text) ? "NestJS" : /\bfastify\b/i.test(text) ? "Fastify" : /\bhono\b/.test(text) ? "Hono" : /\bkoa\b/.test(text) ? "Koa" : "Express";
2324
+ return jsRoutes(rel, text, guess).map((r) => [r.method, r.path, r.framework, r.line ?? null]);
2325
+ }
2326
+ };
2327
+ var NEXT_APP_RE = /(^|\/)(src\/)?app\/(.+\/)?route\.(t|j)sx?$/;
2328
+ var nextRouteFact = {
2329
+ id: "routes-next-app",
2330
+ applies: (f) => NEXT_APP_RE.test(f.path),
2331
+ compute(text) {
2332
+ const out = [];
2333
+ if (!text) return out;
2334
+ for (const m of text.matchAll(/export\s+(?:async\s+)?(?:function|const)\s+(GET|POST|PUT|PATCH|DELETE|HEAD|OPTIONS)\b/g)) out.push([m[1], text.slice(0, m.index).split("\n").length]);
2335
+ return out;
2336
+ }
2337
+ };
2338
+ async function nextRoutes(ctx) {
2339
+ const out = [];
2340
+ for (const f of ctx.find(NEXT_APP_RE)) {
2341
+ const rows = await ctx.fact(f.path, nextRouteFact);
2342
+ if (!rows) continue;
2343
+ const seg = /(?:^|\/)(?:src\/)?app\/(.*)route\.(?:t|j)sx?$/.exec(f.path)?.[1] ?? "";
2344
+ const urlPath = `/${seg.replace(/\/$/, "")}`.split("/").filter((s) => !/^\(.*\)$/.test(s) && !s.startsWith("@")).join("/") || "/";
2345
+ for (const [method, line] of rows) out.push({ method, path: urlPath, framework: "Next.js (app router)", file: f.path, line, provenance: detected("code", [{ file: f.path, line }]) });
2346
+ }
2347
+ for (const f of ctx.find(/(^|\/)(src\/)?pages\/api\/.+\.(t|j)sx?$/)) {
2348
+ const seg = /(?:^|\/)(?:src\/)?pages(\/api\/.+)\.(?:t|j)sx?$/.exec(f.path)?.[1] ?? "";
2349
+ out.push({ method: "ANY", path: seg.replace(/\/index$/, ""), framework: "Next.js (pages API)", file: f.path, provenance: detected("filesystem", [{ file: f.path }]) });
2350
+ }
2351
+ return out;
2352
+ }
2353
+ var pyRoutesFact = {
2354
+ id: "routes-python",
2355
+ applies: (f) => /\.py$/.test(f.path),
2356
+ compute(text) {
2357
+ if (!text || !/@\w+\.(get|post|put|patch|delete|route|api_route)\(/.test(text)) return [];
2358
+ return regexRoutes(text, /@(\w+)\.(get|post|put|patch|delete|options|head|route|api_route)\(\s*["']([^"']*)["']([^\n]*)/g, (m) => {
2359
+ const verb = m[2];
2360
+ if (verb === "route" || verb === "api_route") {
2361
+ const methods = /methods\s*=\s*\[([^\]]+)\]/.exec(m[4])?.[1]?.replace(/["'\s]/g, "").toUpperCase();
2362
+ return { method: methods || "GET", path: m[3] };
2363
+ }
2364
+ return { method: verb.toUpperCase(), path: m[3] };
2365
+ });
2366
+ }
2367
+ };
2368
+ var DJANGO_URLS_RE = /(^|\/)urls\.py$/;
2369
+ var djangoRoutesFact = {
2370
+ id: "routes-django",
2371
+ applies: (f) => DJANGO_URLS_RE.test(f.path),
2372
+ compute: (text) => text ? regexRoutes(text, /\b(re_)?path\(\s*r?["']([^"']*)["']\s*,\s*([\w.]+)/g, (m) => ({ method: "ANY", path: `/${m[2]}` })) : []
2373
+ };
2374
+ var JAVA_RE = /\.(java|kt)$/;
2375
+ var springRoutesFact = {
2376
+ id: "routes-spring",
2377
+ applies: (f) => JAVA_RE.test(f.path),
2378
+ compute(text) {
2379
+ if (!text || !/@(Get|Post|Put|Patch|Delete|Request)Mapping/.test(text)) return [];
2380
+ const prefix = /@RequestMapping\(\s*(?:value\s*=\s*|path\s*=\s*)?["']([^"']*)["']\s*\)\s*(?:@\w+(?:\([^)]*\))?\s*)*(?:public\s+)?(?:abstract\s+)?(?:class|interface)/.exec(text)?.[1] ?? "";
2381
+ return regexRoutes(text, /@(Get|Post|Put|Patch|Delete)Mapping(?:\(\s*(?:value\s*=\s*|path\s*=\s*)?\{?\s*["']([^"']*)["'])?/g, (m) => ({ method: m[1].toUpperCase(), path: `${prefix}${m[2] ?? ""}` || "/" }));
2382
+ }
2383
+ };
2384
+ var goRoutesFact = {
2385
+ id: "routes-go",
2386
+ applies: (f) => /\.go$/.test(f.path) && !f.path.endsWith("_test.go"),
2387
+ compute(text) {
2388
+ if (!text) return [];
2389
+ return regexRoutes(text, /\.(GET|POST|PUT|PATCH|DELETE|Get|Post|Put|Patch|Delete|HandleFunc|Handle)\(\s*"([^"]+)"/g, (m) => {
2390
+ const verb = m[1];
2391
+ if (verb.startsWith("Handle")) {
2392
+ const sp = /^(GET|POST|PUT|PATCH|DELETE|HEAD|OPTIONS)\s+(\/.*)$/.exec(m[2]);
2393
+ if (sp) return { method: sp[1], path: sp[2] };
2394
+ return m[2].startsWith("/") ? { method: "ANY", path: m[2] } : null;
2395
+ }
2396
+ return m[2].startsWith("/") ? { method: verb.toUpperCase(), path: m[2] } : null;
2397
+ });
2398
+ }
2399
+ };
2400
+ var rustRoutesFact = {
2401
+ id: "routes-rust",
2402
+ applies: (f) => /\.rs$/.test(f.path),
2403
+ compute(text) {
2404
+ if (!text) return { axum: [], actix: [] };
2405
+ return {
2406
+ axum: regexRoutes(text, /\.route\(\s*"([^"]+)"\s*,\s*(get|post|put|patch|delete)/g, (m) => ({ method: m[2].toUpperCase(), path: m[1] })),
2407
+ actix: regexRoutes(text, /#\[(get|post|put|patch|delete)\(\s*"([^"]+)"/g, (m) => ({ method: m[1].toUpperCase(), path: m[2] }))
2408
+ };
2409
+ }
2410
+ };
2411
+ var railsRoutesFact = {
2412
+ id: "routes-rails",
2413
+ applies: (f) => f.path === "config/routes.rb",
2414
+ compute(text) {
2415
+ if (!text) return false;
2416
+ return [
2417
+ ...regexRoutes(text, /^\s*(get|post|put|patch|delete)\s+["']([^"']+)["']/gm, (m) => ({ method: m[1].toUpperCase(), path: m[2].startsWith("/") ? m[2] : `/${m[2]}` })),
2418
+ ...regexRoutes(text, /^\s*resources?\s+:(\w+)/gm, (m) => ({ method: "RESOURCE", path: `/${m[1]}` }))
2419
+ ];
2420
+ }
2421
+ };
2422
+ var LARAVEL_RE = /(^|\/)routes\/[^/]+\.php$/;
2423
+ var laravelRoutesFact = {
2424
+ id: "routes-laravel",
2425
+ applies: (f) => LARAVEL_RE.test(f.path),
2426
+ compute: (text) => text ? regexRoutes(text, /Route::(get|post|put|patch|delete|any|resource|apiResource)\(\s*["']([^"']+)["']/g, (m) => ({ method: m[1].toUpperCase(), path: m[2].startsWith("/") ? m[2] : `/${m[2]}` })) : []
2427
+ };
2428
+ var aspnetRoutesFact = {
2429
+ id: "routes-aspnet",
2430
+ applies: (f) => /\.cs$/.test(f.path),
2431
+ compute(text) {
2432
+ if (!text) return [];
2433
+ const rows = regexRoutes(text, /\.Map(Get|Post|Put|Patch|Delete)\(\s*"([^"]+)"/g, (m) => ({ method: m[1].toUpperCase(), path: m[2] }));
2434
+ if (/\[Http(Get|Post|Put|Patch|Delete)/.test(text)) {
2435
+ const prefix = /\[Route\(\s*"([^"]+)"\s*\)\]\s*(?:\[[^\]]+\]\s*)*public\s+(?:sealed\s+|partial\s+)*class/.exec(text)?.[1] ?? "";
2436
+ rows.push(...regexRoutes(text, /\[Http(Get|Post|Put|Patch|Delete)(?:\(\s*"([^"]*)"\s*\))?\]/g, (m) => ({ method: m[1].toUpperCase(), path: `/${[prefix, m[2] ?? ""].filter(Boolean).join("/")}` })));
2437
+ }
2438
+ return rows;
2439
+ }
2440
+ };
2441
+ var routesDetector = {
2442
+ id: "routes",
2443
+ version: 1,
2444
+ facts: [jsRoutesFact, nextRouteFact, pyRoutesFact, djangoRoutesFact, springRoutesFact, goRoutesFact, rustRoutesFact, railsRoutesFact, laravelRoutesFact, aspnetRoutesFact],
2445
+ async run(ctx) {
2446
+ const { model } = ctx;
2447
+ const routes = [];
2448
+ const fw2 = new Set(model.frameworks.map((f) => f.name));
2449
+ const push = (rs) => {
2450
+ for (const r of rs) if (routes.length < MAX_ROUTES) routes.push(r);
2451
+ };
2452
+ if (fw2.has("Next.js")) push(await nextRoutes(ctx));
2453
+ if (!fw2.has("Next.js") && (ctx.has("vercel.json") || fw2.has("Vercel Functions (Node)"))) {
2454
+ for (const f of ctx.find(/^api\/.+\.(m|c)?(t|j)s$/)) {
2455
+ if (/(^|\/)_|\.(test|spec|d)\./.test(f.path)) continue;
2456
+ const urlPath = `/${f.path.replace(/\.(m|c)?(t|j)s$/, "").replace(/\/index$/, "")}`;
2457
+ routes.push({ method: "ANY", path: urlPath, framework: "Vercel Functions", file: f.path, provenance: detected("filesystem", [{ file: f.path, detail: "Vercel api/ directory convention" }], "medium") });
2458
+ }
2459
+ }
2460
+ const jsFrameworks = JS_FRAMEWORKS.filter((n) => fw2.has(n));
2461
+ if (jsFrameworks.length) {
2462
+ for (const f of ctx.find(JS_RE)) {
2463
+ if (f.large || /\.(test|spec|d)\.[cm]?[jt]sx?$/.test(f.path)) continue;
2464
+ const rows = await ctx.fact(f.path, jsRoutesFact);
2465
+ if (!rows) continue;
2466
+ push(rows.map(([method, path7, framework, line]) => ({ method, path: path7, framework, file: f.path, line: line ?? void 0, provenance: detected("code", [{ file: f.path, line: line ?? void 0 }], "high") })));
2467
+ }
2468
+ }
2469
+ const pyFw = ["FastAPI", "Flask", "Starlette", "Litestar"].some((n) => fw2.has(n));
2470
+ if (pyFw) {
2471
+ for (const f of ctx.find(/\.py$/)) push(toRoutes(f.path, fw2.has("FastAPI") ? "FastAPI" : "Flask", await ctx.fact(f.path, pyRoutesFact)));
2472
+ }
2473
+ if (fw2.has("Django")) {
2474
+ for (const f of ctx.find(DJANGO_URLS_RE)) push(toRoutes(f.path, "Django", await ctx.fact(f.path, djangoRoutesFact)));
2475
+ }
2476
+ for (const f of ctx.find(JAVA_RE)) push(toRoutes(f.path, "Spring", await ctx.fact(f.path, springRoutesFact)));
2477
+ for (const f of ctx.find(/\.go$/)) {
2478
+ if (f.path.endsWith("_test.go")) continue;
2479
+ push(toRoutes(f.path, "Go", await ctx.fact(f.path, goRoutesFact)));
2480
+ }
2481
+ for (const f of ctx.find(/\.rs$/)) {
2482
+ const v = await ctx.fact(f.path, rustRoutesFact);
2483
+ if (!v) continue;
2484
+ push(toRoutes(f.path, "Axum", v.axum));
2485
+ push(toRoutes(f.path, "Actix/Rocket", v.actix));
2486
+ }
2487
+ if (ctx.has("config/routes.rb")) {
2488
+ const rows = await ctx.fact("config/routes.rb", railsRoutesFact);
2489
+ if (rows) push(toRoutes("config/routes.rb", "Rails", rows));
2490
+ }
2491
+ for (const f of ctx.find(LARAVEL_RE)) push(toRoutes(f.path, "Laravel", await ctx.fact(f.path, laravelRoutesFact)));
2492
+ for (const f of ctx.find(/\.cs$/)) push(toRoutes(f.path, "ASP.NET Core", await ctx.fact(f.path, aspnetRoutesFact)));
2493
+ model.routes = routes.sort((a, b) => a.path.localeCompare(b.path) || a.method.localeCompare(b.method));
2494
+ if (routes.length >= MAX_ROUTES) ctx.warn(`Route listing truncated at ${MAX_ROUTES} entries.`);
2495
+ for (const f of ctx.find(/(^|\/)(openapi|swagger)[^/]*\.(json|ya?ml)$/i)) model.apiSpecs.push({ path: f.path, kind: "openapi" });
2496
+ for (const f of ctx.find(/\.(graphql|gql)$/)) model.apiSpecs.push({ path: f.path, kind: "graphql" });
2497
+ for (const f of ctx.find(/\.proto$/)) model.apiSpecs.push({ path: f.path, kind: "grpc" });
2498
+ }
2499
+ };
2500
+
2501
+ // src/core/analyzer/detectors/auth.ts
2502
+ var MIDDLEWARE_RE = /(^|\/)(src\/)?middleware\.(t|j)s$/;
2503
+ var ROLE_FILE_RE = /\.(prisma|ts|tsx|py|java|kt|cs|go|rb|php)$/;
2504
+ var MAX_ROLE_EVIDENCE = 10;
2505
+ var middlewareFact = {
2506
+ id: "auth-middleware",
2507
+ applies: (f) => MIDDLEWARE_RE.test(f.path),
2508
+ compute: (text) => !!text && /(auth|session|token|redirect\(.*login)/i.test(text)
2509
+ };
2510
+ var roleDefsFact = {
2511
+ id: "auth-role-defs",
2512
+ applies: (f) => ROLE_FILE_RE.test(f.path) && !f.large,
2513
+ compute(text) {
2514
+ const out = [];
2515
+ if (!text || !/role|permission/i.test(text)) return out;
2516
+ for (const m of text.matchAll(/\b(?:enum|class|type|interface)\s+(\w*(?:Role|Permission)s?)\b/g)) {
2517
+ out.push([m[1], text.slice(0, m.index).split("\n").length]);
2518
+ if (out.length >= MAX_ROLE_EVIDENCE) break;
2519
+ }
2520
+ return out;
2521
+ }
2522
+ };
2523
+ var authDetector = {
2524
+ id: "auth",
2525
+ version: 1,
2526
+ facts: [middlewareFact, roleDefsFact],
2527
+ async run(ctx) {
2528
+ const { model } = ctx;
2529
+ const authFiles = ctx.find((f) => !f.binary && /\.(m|c)?(t|j)sx?$|\.(py|go|rs|java|kt|cs|php|rb|ex)$/.test(f.path) && /(^|\/)[^/]*(auth|session|login|oauth|jwt|guard|permission|rbac|acl|polic(y|ies))[^/]*$/i.test(f.path) && !/\.(test|spec)\./.test(f.path));
2530
+ if (authFiles.length) {
2531
+ model.auth.push({
2532
+ name: `Auth-related source files (${authFiles.length})`,
2533
+ kind: "middleware",
2534
+ provenance: inferred("filesystem", authFiles.slice(0, 8).map((f) => ({ file: f.path, detail: "file name suggests auth concern" })), "low")
2535
+ });
2536
+ }
2537
+ for (const f of ctx.find(MIDDLEWARE_RE)) {
2538
+ if (await ctx.fact(f.path, middlewareFact)) {
2539
+ model.auth.push({ name: "Next.js middleware referencing auth/session", kind: "middleware", provenance: detected("code", [{ file: f.path }], "medium") });
2540
+ }
2541
+ }
2542
+ const roleEvidence = [];
2543
+ for (const f of ctx.find(ROLE_FILE_RE)) {
2544
+ if (f.large || roleEvidence.length >= MAX_ROLE_EVIDENCE) continue;
2545
+ const defs = await ctx.fact(f.path, roleDefsFact);
2546
+ if (!defs) continue;
2547
+ for (const [name, line] of defs) {
2548
+ roleEvidence.push({ file: f.path, line, detail: `definition "${name}"` });
2549
+ if (roleEvidence.length >= MAX_ROLE_EVIDENCE) break;
2550
+ }
2551
+ }
2552
+ for (const e of model.dbEntities) {
2553
+ if (/^(roles?|permissions?|user_roles?|role_permissions?)$/i.test(e.name)) roleEvidence.push({ file: e.file, line: e.provenance.evidence[0]?.line ?? 1, detail: `database entity "${e.name}"` });
2554
+ }
2555
+ if (roleEvidence.length) {
2556
+ model.auth.push({ name: "Role/permission definitions", kind: "middleware", provenance: detected("code", roleEvidence, "medium") });
2557
+ }
2558
+ }
2559
+ };
2560
+
2561
+ // src/core/analyzer/detectors/entrypoints.ts
2562
+ var CONVENTIONAL = /(^|\/)(src\/)?(main|index|server|app|cli)\.(m|c)?(t|j)sx?$|(^|\/)(main|app|wsgi|asgi|manage|__main__)\.py$|(^|\/)(cmd\/[^/]+\/)?main\.go$|(^|\/)src\/(main|lib)\.rs$|(^|\/)Program\.cs$|(^|\/)[A-Za-z]+Application\.(java|kt)$|(^|\/)lib\/main\.dart$|(^|\/)public\/index\.php$|(^|\/)config\.ru$/;
2563
+ var entryPointsDetector = {
2564
+ id: "entrypoints",
2565
+ version: 1,
2566
+ async run(ctx) {
2567
+ const known = new Set(ctx.model.entryPoints.map((e) => e.path));
2568
+ const candidates = ctx.find((f) => CONVENTIONAL.test(f.path) && f.path.split("/").length <= 5 && !/(^|\/)(test|tests|__tests__|examples?|fixtures?)\//.test(f.path));
2569
+ for (const f of candidates.slice(0, 30)) {
2570
+ if (known.has(f.path)) continue;
2571
+ ctx.model.entryPoints.push({ path: f.path, provenance: inferred("filesystem", [{ file: f.path, detail: "conventional entry-point file name" }], "medium") });
2572
+ }
2573
+ }
2574
+ };
2575
+
2576
+ // src/core/analyzer/detectors/secrets.ts
2577
+ import { promises as fs4 } from "fs";
2578
+ import path4 from "path";
2579
+ import crypto2 from "crypto";
2580
+ var SKIP = /(^|\/)(package-lock\.json|yarn\.lock|pnpm-lock\.yaml|bun\.lock|poetry\.lock|uv\.lock|Cargo\.lock|go\.sum|composer\.lock|Gemfile\.lock|pubspec\.lock)$|\.(min\.js|map|svg|snap)$/;
2581
+ var MAX_SECRET_FINDINGS = 500;
2582
+ async function secretFingerprinter(root) {
2583
+ return (await secretFingerprintKey(root)).fingerprint;
2584
+ }
2585
+ async function secretFingerprintKey(root, saltOverride) {
2586
+ try {
2587
+ let salt = saltOverride;
2588
+ if (!salt) salt = (await readLocalConfig(root)).salt;
2589
+ if (!salt) {
2590
+ const hasDir = await fs4.stat(path4.join(root, ATHENA_DIR)).then((s) => s.isDirectory(), () => false);
2591
+ if (hasDir) salt = await getOrCreateSalt(root);
2592
+ }
2593
+ if (salt) {
2594
+ const key = salt;
2595
+ return { fingerprint: (value) => keyedFingerprint(key, value), fingerprintKeyId: crypto2.createHash("sha256").update(`athena-fingerprint-key:${key}`).digest("hex").slice(0, 12) };
2596
+ }
2597
+ } catch {
2598
+ }
2599
+ return { fingerprint: ephemeralFingerprint, fingerprintKeyId: null };
2600
+ }
2601
+ async function scanFilesForSecrets(files, read, opts) {
2602
+ const findings = opts.into ?? [];
2603
+ const max = opts.max ?? MAX_SECRET_FINDINGS;
2604
+ const stats = { skippedLongLines: 0 };
2605
+ let truncated = false;
2606
+ for (const f of files) {
2607
+ if (f.binary || f.large || SKIP.test(f.path)) continue;
2608
+ if (findings.length >= max) {
2609
+ truncated = true;
2610
+ break;
2611
+ }
2612
+ opts.signal?.throwIfAborted();
2613
+ const text = await read(f.path);
2614
+ if (!text) continue;
2615
+ for (const m of scanText(text, { fingerprint: opts.fingerprint, stats })) findings.push({ type: m.type, file: f.path, line: m.line, fingerprint: m.fingerprint });
2616
+ }
2617
+ return { findings, truncated, skippedLongLines: stats.skippedLongLines };
2618
+ }
2619
+ var secretsFact = {
2620
+ id: "secrets",
2621
+ keyed: true,
2622
+ applies: (f) => !f.binary && !f.large && !SKIP.test(f.path),
2623
+ compute(text, _rel, rt) {
2624
+ if (!text) return [];
2625
+ return scanText(text, { fingerprint: rt.fingerprint }).map((m) => [m.type, m.line, m.fingerprint]);
2626
+ }
2627
+ };
2628
+ var secretsDetector = {
2629
+ id: "secrets",
2630
+ version: 2,
2631
+ facts: [secretsFact],
2632
+ async run(ctx) {
2633
+ const findings = ctx.model.security.secrets;
2634
+ let truncated = false;
2635
+ for (const f of ctx.files) {
2636
+ if (f.binary || f.large || SKIP.test(f.path)) continue;
2637
+ if (findings.length >= MAX_SECRET_FINDINGS) {
2638
+ truncated = true;
2639
+ break;
2640
+ }
2641
+ ctx.signal?.throwIfAborted();
2642
+ const rows = await ctx.fact(f.path, secretsFact);
2643
+ if (!rows) continue;
2644
+ for (const [type, line, fingerprint] of rows) findings.push({ type, file: f.path, line, fingerprint });
2645
+ }
2646
+ if (truncated) ctx.warn(`Secret scan stopped after ${MAX_SECRET_FINDINGS} findings.`);
2647
+ }
2648
+ };
2649
+
2650
+ // src/core/analyzer/analyze.ts
2651
+ var DETECTORS = [
2652
+ structureDetector,
2653
+ manifestsDetector,
2654
+ dependenciesDetector,
2655
+ envDetector,
2656
+ infraDetector,
2657
+ testsDetector,
2658
+ databaseDetector,
2659
+ routesDetector,
2660
+ authDetector,
2661
+ entryPointsDetector,
2662
+ secretsDetector
2663
+ ];
2664
+ var FACTS_REVISION = 1;
2665
+ var DETECTORS_VERSION = `${FACTS_REVISION};${DETECTORS.map((d) => `${d.id}@${d.version}`).join(",")}`;
2666
+ var ALL_FACTS = DETECTORS.flatMap((d) => d.facts ?? []);
2667
+ var pendingCaches = /* @__PURE__ */ new WeakMap();
2668
+ async function isDir(p) {
2669
+ return fs5.stat(p).then((s) => s.isDirectory(), () => false);
2670
+ }
2671
+ async function analyzeProject(rootInput, opts = {}) {
2672
+ const started = Date.now();
2673
+ const root = await fs5.realpath(path5.resolve(rootInput));
2674
+ const loaded = opts.config ? { config: opts.config } : await loadConfig(root);
2675
+ const config = loaded.config;
2676
+ const model = emptyModel(path5.basename(root), root);
2677
+ if ("warning" in loaded && loaded.warning) model.warnings.push(loaded.warning);
2678
+ const athena = path5.join(root, ATHENA_DIR);
2679
+ const useCache = opts.cache !== false && await isDir(athena);
2680
+ const reuse = opts.reuse ?? (useCache ? await readFileIndex(athena) : void 0);
2681
+ opts.onStage?.("scan");
2682
+ const configHash = factsConfigHash(config);
2683
+ const walkStarted = Date.now();
2684
+ const [excludeText, prevWalk] = await Promise.all([readExclude(root), opts.incremental ? incrementalWalk(root, config, configHash, opts.incremental, opts.signal) : null]);
2685
+ const matcher = prevWalk ? opts.incremental.snapshot.matcher : await IgnoreMatcher.load(root, config);
2686
+ const walk2 = prevWalk ?? await walkProject(root, { config, signal: opts.signal, reuse, deferRead: true, matcher });
2687
+ model.warnings.push(...walk2.warnings);
2688
+ const runtime = await secretFingerprintKey(root, opts.fingerprintSalt);
2689
+ const cache = await FactsCache.open(useCache ? athena : null, { detectors: DETECTORS_VERSION, config: configHash });
2690
+ const engine = new FactEngine({ root, files: walk2.files, cache, runtime, defs: ALL_FACTS, signal: opts.signal, readFile: opts.readFile, textCacheBytes: opts.textCacheBytes });
2691
+ await cache.preload(walk2.files.filter((f) => f.hash && !f.binary && !f.large).map((f) => f.hash));
2692
+ const gated = ALL_FACTS.filter((d) => d.when);
2693
+ const needHints = walk2.files.some((f) => !f.large && gated.some((d) => d.applies(f) && (f.hash === "" || !f.binary && !cache.has(f.hash, engine.keyOf(d, f.path)))));
2694
+ engine.hints = needHints ? await projectHints(root, config, walk2.files, engine, opts.signal) : { frameworks: /* @__PURE__ */ new Set() };
2695
+ const todo = walk2.files.filter((f) => engine.needsProcessing(f));
2696
+ await mapLimit(todo, 32, (f) => engine.process(f));
2697
+ opts.signal?.throwIfAborted();
2698
+ const files = engine.unreadable.size ? walk2.files.filter((f) => !engine.unreadable.has(f.path)) : walk2.files;
2699
+ let skippedBinary = 0;
2700
+ let skippedLarge = 0;
2701
+ for (const f of files) {
2702
+ if (f.binary) skippedBinary++;
2703
+ if (f.large) skippedLarge++;
2704
+ }
2705
+ model.stats = { filesScanned: files.length, skippedBinary, skippedLarge, skippedUnreadable: walk2.skippedUnreadable + engine.unreadable.size };
2706
+ opts.onStage?.("detect");
2707
+ const ctx = createContext(root, config, files, model, engine, opts.signal);
2708
+ for (const d of DETECTORS) {
2709
+ opts.signal?.throwIfAborted();
2710
+ const t = Date.now();
2711
+ try {
2712
+ await d.run(ctx);
2713
+ } catch (err) {
2714
+ if (opts.signal?.aborted) throw err;
2715
+ model.warnings.push(`Detector "${d.id}" failed: ${err.message}`);
2716
+ }
2717
+ opts.onDetector?.(d.id, Date.now() - t);
2718
+ }
2719
+ opts.onStage?.("git");
2720
+ model.git = await gitInfo(root);
2721
+ opts.onStage?.("finalize");
2722
+ const redacted = redactDeep(model);
2723
+ const check = ProjectModel.safeParse(redacted);
2724
+ if (!check.success) redacted.warnings.push(`Internal model validation issue: ${check.error.issues[0]?.path.join(".")} ${check.error.issues[0]?.message}`);
2725
+ const live = new Set(files.map((f) => f.hash));
2726
+ const result = {
2727
+ model: redacted,
2728
+ files,
2729
+ config,
2730
+ detectorVersions: Object.fromEntries(DETECTORS.map((d) => [d.id, d.version])),
2731
+ durationMs: 0,
2732
+ perf: { ...engine.stats, cache: cache.stats },
2733
+ walkMode: prevWalk ? "incremental" : "full",
2734
+ snapshot: {
2735
+ files,
2736
+ configHash,
2737
+ maxFiles: config.maxFiles,
2738
+ excludeText,
2739
+ matcher,
2740
+ // An incremental walk inherits reusability from the snapshot it started from.
2741
+ reusable: engine.unreadable.size === 0 && (prevWalk ? true : !walk2.truncated && walk2.symlinks === 0 && walk2.skippedUnreadable === 0),
2742
+ takenAt: prevWalk ? opts.incremental.snapshot.takenAt : walkStarted
2743
+ }
2744
+ };
2745
+ if (useCache) {
2746
+ try {
2747
+ await cache.save(athena, live);
2748
+ } catch {
2749
+ }
2750
+ } else if (opts.cache !== false) pendingCaches.set(result, { cache, live });
2751
+ result.durationMs = Date.now() - started;
2752
+ return result;
2753
+ }
2754
+ async function persistAnalysisCache(result, athenaDir2) {
2755
+ const pending = pendingCaches.get(result);
2756
+ if (!pending) return;
2757
+ pendingCaches.delete(result);
2758
+ try {
2759
+ await pending.cache.save(athenaDir2, pending.live);
2760
+ } catch {
2761
+ }
2762
+ }
2763
+ async function projectHints(root, config, files, engine, signal) {
2764
+ const scratch = emptyModel(path5.basename(root), root);
2765
+ const ctx = createContext(root, config, files, scratch, engine, signal);
2766
+ try {
2767
+ await manifestsDetector.run(ctx);
2768
+ await dependenciesDetector.run(ctx);
2769
+ } catch (err) {
2770
+ if (signal?.aborted) throw err;
2771
+ }
2772
+ return { frameworks: new Set(scratch.frameworks.map((f) => f.name)) };
2773
+ }
2774
+ function redactDeep(value, memo = /* @__PURE__ */ new Map()) {
2775
+ if (typeof value === "string") {
2776
+ let r = memo.get(value);
2777
+ if (r === void 0) {
2778
+ r = redact(value);
2779
+ memo.set(value, r);
2780
+ }
2781
+ return r;
2782
+ }
2783
+ if (Array.isArray(value)) return value.map((v) => redactDeep(v, memo));
2784
+ if (value && typeof value === "object") {
2785
+ const out = {};
2786
+ for (const [k, v] of Object.entries(value)) out[k] = redactDeep(v, memo);
2787
+ return out;
2788
+ }
2789
+ return value;
2790
+ }
2791
+
2792
+ // src/core/model/security-scan.ts
2793
+ import { z as z3 } from "zod";
2794
+ import path6 from "path";
2795
+ var SCAN_FILE = "security-scan.json";
2796
+ var SEVERITIES = ["critical", "high", "moderate", "low", "unrated"];
2797
+ var SEVERITY_ORDER = [...SEVERITIES];
2798
+ var RATED_SEVERITIES = ["critical", "high", "moderate", "low"];
2799
+ var legacySeverity = (v) => v === "unknown" ? "unrated" : v;
2800
+ var Severity = z3.preprocess(legacySeverity, z3.enum(SEVERITIES));
2801
+ var Vulnerability = z3.object({
2802
+ package: z3.string(),
2803
+ severity: Severity,
2804
+ title: z3.string(),
2805
+ id: z3.string().optional(),
2806
+ url: z3.string().optional(),
2807
+ vulnerableRange: z3.string().optional(),
2808
+ fixAvailable: z3.boolean().optional()
2809
+ });
2810
+ var ToolResult = z3.object({
2811
+ tool: z3.string(),
2812
+ ecosystem: z3.string(),
2813
+ /** What was audited when a tool runs once per input (e.g. a requirements file). */
2814
+ target: z3.string().optional(),
2815
+ /** ok: ran and produced results · unavailable: not installed / nothing to audit · failed/timeout: ran but no usable output */
2816
+ status: z3.enum(["ok", "unavailable", "failed", "timeout"]),
2817
+ message: z3.string().optional(),
2818
+ durationMs: z3.number(),
2819
+ findings: z3.array(Vulnerability)
2820
+ });
2821
+ var Counts = z3.preprocess(
2822
+ (v) => {
2823
+ if (!v || typeof v !== "object") return v;
2824
+ const { unknown, ...rest } = v;
2825
+ return { critical: 0, high: 0, moderate: 0, low: 0, unrated: 0, ...rest, ...typeof unknown === "number" ? { unrated: (rest.unrated ?? 0) + unknown } : {} };
2826
+ },
2827
+ z3.object({ critical: z3.number(), high: z3.number(), moderate: z3.number(), low: z3.number(), unrated: z3.number() })
2828
+ );
2829
+ var SecurityScan = z3.object({
2830
+ schemaVersion: z3.literal(1),
2831
+ scannedAt: z3.string(),
2832
+ durationMs: z3.number(),
2833
+ tools: z3.array(ToolResult),
2834
+ counts: Counts,
2835
+ secrets: z3.object({
2836
+ count: z3.number(),
2837
+ files: z3.array(z3.string()),
2838
+ /** Lines longer than 4096 chars on which generic keyword patterns were not applied. */
2839
+ skippedLongLines: z3.number().optional(),
2840
+ truncated: z3.boolean().optional()
2841
+ })
2842
+ });
2843
+ var loadScan = (root) => loadScanFromDir(athenaDir(root));
2844
+ async function saveScan(root, scan) {
2845
+ await writeFileAtomic(path6.join(athenaDir(root), SCAN_FILE), `${JSON.stringify(scan, null, 2)}
2846
+ `);
2847
+ }
2848
+ async function loadScanFromDir(dir) {
2849
+ const raw = await readTextIfExists(path6.join(dir, SCAN_FILE));
2850
+ if (!raw) return null;
2851
+ try {
2852
+ const parsed = SecurityScan.safeParse(JSON.parse(raw));
2853
+ return parsed.success ? parsed.data : null;
2854
+ } catch {
2855
+ return null;
2856
+ }
2857
+ }
2858
+ function unratedFindings(scan) {
2859
+ return scan.tools.flatMap((t) => t.status === "ok" ? t.findings.filter((f) => f.severity === "unrated") : []);
2860
+ }
2861
+ function meetsThreshold(scan, level, opts = {}) {
2862
+ const max = SEVERITY_ORDER.indexOf(level);
2863
+ const unrated = opts.unrated ?? "fail";
2864
+ return scan.tools.some(
2865
+ (t) => t.findings.some((f) => {
2866
+ if (f.severity === "unrated") return unrated === "fail";
2867
+ return SEVERITY_ORDER.indexOf(f.severity) <= max;
2868
+ })
2869
+ );
2870
+ }
2871
+
2872
+ export {
2873
+ ProjectModel,
2874
+ readExclude,
2875
+ manifestDependencyNames,
2876
+ REAL_ENV_NAMES,
2877
+ secretFingerprinter,
2878
+ scanFilesForSecrets,
2879
+ analyzeProject,
2880
+ persistAnalysisCache,
2881
+ redactDeep,
2882
+ SCAN_FILE,
2883
+ SEVERITY_ORDER,
2884
+ RATED_SEVERITIES,
2885
+ SecurityScan,
2886
+ loadScan,
2887
+ saveScan,
2888
+ loadScanFromDir,
2889
+ unratedFindings,
2890
+ meetsThreshold
2891
+ };
2892
+ //# sourceMappingURL=chunk-PANVPNYF.js.map