@vyuhlabs/dxkit 2.5.2 → 2.7.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 (264) hide show
  1. package/CHANGELOG.md +218 -13
  2. package/README.md +220 -369
  3. package/dist/allowlist/categories.d.ts +120 -0
  4. package/dist/allowlist/categories.d.ts.map +1 -0
  5. package/dist/allowlist/categories.js +194 -0
  6. package/dist/allowlist/categories.js.map +1 -0
  7. package/dist/allowlist/cli.d.ts +95 -0
  8. package/dist/allowlist/cli.d.ts.map +1 -0
  9. package/dist/allowlist/cli.js +454 -0
  10. package/dist/allowlist/cli.js.map +1 -0
  11. package/dist/allowlist/diff.d.ts +67 -0
  12. package/dist/allowlist/diff.d.ts.map +1 -0
  13. package/dist/allowlist/diff.js +147 -0
  14. package/dist/allowlist/diff.js.map +1 -0
  15. package/dist/allowlist/file.d.ts +249 -0
  16. package/dist/allowlist/file.d.ts.map +1 -0
  17. package/dist/allowlist/file.js +497 -0
  18. package/dist/allowlist/file.js.map +1 -0
  19. package/dist/allowlist/gather.d.ts +61 -0
  20. package/dist/allowlist/gather.d.ts.map +1 -0
  21. package/dist/allowlist/gather.js +143 -0
  22. package/dist/allowlist/gather.js.map +1 -0
  23. package/dist/allowlist/hint.d.ts +80 -0
  24. package/dist/allowlist/hint.d.ts.map +1 -0
  25. package/dist/allowlist/hint.js +271 -0
  26. package/dist/allowlist/hint.js.map +1 -0
  27. package/dist/allowlist/inline.d.ts +149 -0
  28. package/dist/allowlist/inline.d.ts.map +1 -0
  29. package/dist/allowlist/inline.js +306 -0
  30. package/dist/allowlist/inline.js.map +1 -0
  31. package/dist/analyzers/bom/discovery.d.ts +3 -4
  32. package/dist/analyzers/bom/discovery.d.ts.map +1 -1
  33. package/dist/analyzers/bom/discovery.js +3 -4
  34. package/dist/analyzers/bom/discovery.js.map +1 -1
  35. package/dist/analyzers/bom/types.d.ts +1 -1
  36. package/dist/analyzers/dashboard/index.d.ts.map +1 -1
  37. package/dist/analyzers/dashboard/index.js +42 -5
  38. package/dist/analyzers/dashboard/index.js.map +1 -1
  39. package/dist/analyzers/quality/detailed.d.ts +8 -1
  40. package/dist/analyzers/quality/detailed.d.ts.map +1 -1
  41. package/dist/analyzers/quality/detailed.js +43 -10
  42. package/dist/analyzers/quality/detailed.js.map +1 -1
  43. package/dist/analyzers/security/detailed.d.ts +8 -1
  44. package/dist/analyzers/security/detailed.d.ts.map +1 -1
  45. package/dist/analyzers/security/detailed.js +14 -1
  46. package/dist/analyzers/security/detailed.js.map +1 -1
  47. package/dist/analyzers/tests/detailed.d.ts +8 -1
  48. package/dist/analyzers/tests/detailed.d.ts.map +1 -1
  49. package/dist/analyzers/tests/detailed.js +26 -7
  50. package/dist/analyzers/tests/detailed.js.map +1 -1
  51. package/dist/analyzers/tools/cloc.js +3 -3
  52. package/dist/analyzers/tools/cloc.js.map +1 -1
  53. package/dist/analyzers/tools/exclusions.d.ts +12 -12
  54. package/dist/analyzers/tools/exclusions.d.ts.map +1 -1
  55. package/dist/analyzers/tools/exclusions.js +27 -13
  56. package/dist/analyzers/tools/exclusions.js.map +1 -1
  57. package/dist/analyzers/tools/graphify.d.ts +39 -5
  58. package/dist/analyzers/tools/graphify.d.ts.map +1 -1
  59. package/dist/analyzers/tools/graphify.js +609 -45
  60. package/dist/analyzers/tools/graphify.js.map +1 -1
  61. package/dist/analyzers/tools/nuget-package-reference.d.ts +4 -4
  62. package/dist/analyzers/tools/nuget-package-reference.js +4 -4
  63. package/dist/analyzers/tools/osv-scanner-fix.d.ts +4 -5
  64. package/dist/analyzers/tools/osv-scanner-fix.d.ts.map +1 -1
  65. package/dist/analyzers/tools/osv-scanner-fix.js +4 -5
  66. package/dist/analyzers/tools/osv-scanner-fix.js.map +1 -1
  67. package/dist/analyzers/tools/parallel.d.ts.map +1 -1
  68. package/dist/analyzers/tools/parallel.js +7 -0
  69. package/dist/analyzers/tools/parallel.js.map +1 -1
  70. package/dist/analyzers/tools/vendored-advisor.d.ts.map +1 -1
  71. package/dist/analyzers/tools/vendored-advisor.js +3 -4
  72. package/dist/analyzers/tools/vendored-advisor.js.map +1 -1
  73. package/dist/analyzers/xlsx/licenses.d.ts +7 -7
  74. package/dist/analyzers/xlsx/licenses.js +7 -7
  75. package/dist/baseline/baseline-file.d.ts +7 -0
  76. package/dist/baseline/baseline-file.d.ts.map +1 -1
  77. package/dist/baseline/baseline-file.js +22 -1
  78. package/dist/baseline/baseline-file.js.map +1 -1
  79. package/dist/baseline/check-renderers.d.ts +13 -1
  80. package/dist/baseline/check-renderers.d.ts.map +1 -1
  81. package/dist/baseline/check-renderers.js +67 -1
  82. package/dist/baseline/check-renderers.js.map +1 -1
  83. package/dist/baseline/check.d.ts +33 -7
  84. package/dist/baseline/check.d.ts.map +1 -1
  85. package/dist/baseline/check.js +90 -64
  86. package/dist/baseline/check.js.map +1 -1
  87. package/dist/baseline/create.d.ts +35 -7
  88. package/dist/baseline/create.d.ts.map +1 -1
  89. package/dist/baseline/create.js +43 -5
  90. package/dist/baseline/create.js.map +1 -1
  91. package/dist/baseline/entry-to-located.d.ts +6 -1
  92. package/dist/baseline/entry-to-located.d.ts.map +1 -1
  93. package/dist/baseline/entry-to-located.js +20 -2
  94. package/dist/baseline/entry-to-located.js.map +1 -1
  95. package/dist/baseline/finding-identity.d.ts.map +1 -1
  96. package/dist/baseline/finding-identity.js +15 -13
  97. package/dist/baseline/finding-identity.js.map +1 -1
  98. package/dist/baseline/modes.d.ts +140 -0
  99. package/dist/baseline/modes.d.ts.map +1 -0
  100. package/dist/baseline/modes.js +179 -0
  101. package/dist/baseline/modes.js.map +1 -0
  102. package/dist/baseline/policy.d.ts +64 -0
  103. package/dist/baseline/policy.d.ts.map +1 -1
  104. package/dist/baseline/policy.js +102 -1
  105. package/dist/baseline/policy.js.map +1 -1
  106. package/dist/baseline/producers/health.d.ts +2 -2
  107. package/dist/baseline/producers/health.d.ts.map +1 -1
  108. package/dist/baseline/producers/health.js.map +1 -1
  109. package/dist/baseline/producers/index.d.ts +11 -5
  110. package/dist/baseline/producers/index.d.ts.map +1 -1
  111. package/dist/baseline/producers/index.js +12 -9
  112. package/dist/baseline/producers/index.js.map +1 -1
  113. package/dist/baseline/producers/quality.d.ts +3 -3
  114. package/dist/baseline/producers/quality.d.ts.map +1 -1
  115. package/dist/baseline/producers/quality.js.map +1 -1
  116. package/dist/baseline/producers/secret-hmac.d.ts +2 -2
  117. package/dist/baseline/producers/secret-hmac.d.ts.map +1 -1
  118. package/dist/baseline/producers/secret-hmac.js.map +1 -1
  119. package/dist/baseline/producers/security.d.ts +2 -2
  120. package/dist/baseline/producers/security.d.ts.map +1 -1
  121. package/dist/baseline/producers/security.js.map +1 -1
  122. package/dist/baseline/producers/stale-allow.d.ts +70 -0
  123. package/dist/baseline/producers/stale-allow.d.ts.map +1 -0
  124. package/dist/baseline/producers/stale-allow.js +111 -0
  125. package/dist/baseline/producers/stale-allow.js.map +1 -0
  126. package/dist/baseline/producers/tests.d.ts +2 -2
  127. package/dist/baseline/producers/tests.d.ts.map +1 -1
  128. package/dist/baseline/producers/tests.js.map +1 -1
  129. package/dist/baseline/ref-baseline.d.ts +114 -0
  130. package/dist/baseline/ref-baseline.d.ts.map +1 -0
  131. package/dist/baseline/ref-baseline.js +260 -0
  132. package/dist/baseline/ref-baseline.js.map +1 -0
  133. package/dist/baseline/sanitize.d.ts +80 -0
  134. package/dist/baseline/sanitize.d.ts.map +1 -0
  135. package/dist/baseline/sanitize.js +91 -0
  136. package/dist/baseline/sanitize.js.map +1 -0
  137. package/dist/baseline/show.d.ts.map +1 -1
  138. package/dist/baseline/show.js +9 -3
  139. package/dist/baseline/show.js.map +1 -1
  140. package/dist/baseline/types.d.ts +73 -26
  141. package/dist/baseline/types.d.ts.map +1 -1
  142. package/dist/baseline/types.js +7 -1
  143. package/dist/baseline/types.js.map +1 -1
  144. package/dist/baseline/visibility.d.ts +61 -0
  145. package/dist/baseline/visibility.d.ts.map +1 -0
  146. package/dist/baseline/visibility.js +121 -0
  147. package/dist/baseline/visibility.js.map +1 -0
  148. package/dist/cli.d.ts.map +1 -1
  149. package/dist/cli.js +168 -6
  150. package/dist/cli.js.map +1 -1
  151. package/dist/dashboard/graph-adapter.d.ts +151 -0
  152. package/dist/dashboard/graph-adapter.d.ts.map +1 -0
  153. package/dist/dashboard/graph-adapter.js +415 -0
  154. package/dist/dashboard/graph-adapter.js.map +1 -0
  155. package/dist/dashboard/graph-tab.d.ts +109 -0
  156. package/dist/dashboard/graph-tab.d.ts.map +1 -0
  157. package/dist/dashboard/graph-tab.js +297 -0
  158. package/dist/dashboard/graph-tab.js.map +1 -0
  159. package/dist/dashboard/vendor/vis-network.min.js +34 -0
  160. package/dist/doctor.d.ts.map +1 -1
  161. package/dist/doctor.js +106 -16
  162. package/dist/doctor.js.map +1 -1
  163. package/dist/explore/cli/api-surface.d.ts +12 -0
  164. package/dist/explore/cli/api-surface.d.ts.map +1 -0
  165. package/dist/explore/cli/api-surface.js +57 -0
  166. package/dist/explore/cli/api-surface.js.map +1 -0
  167. package/dist/explore/cli/communities.d.ts +10 -0
  168. package/dist/explore/cli/communities.d.ts.map +1 -0
  169. package/dist/explore/cli/communities.js +47 -0
  170. package/dist/explore/cli/communities.js.map +1 -0
  171. package/dist/explore/cli/context.d.ts +16 -0
  172. package/dist/explore/cli/context.d.ts.map +1 -0
  173. package/dist/explore/cli/context.js +118 -0
  174. package/dist/explore/cli/context.js.map +1 -0
  175. package/dist/explore/cli/entry-points.d.ts +12 -0
  176. package/dist/explore/cli/entry-points.d.ts.map +1 -0
  177. package/dist/explore/cli/entry-points.js +85 -0
  178. package/dist/explore/cli/entry-points.js.map +1 -0
  179. package/dist/explore/cli/feature.d.ts +16 -0
  180. package/dist/explore/cli/feature.d.ts.map +1 -0
  181. package/dist/explore/cli/feature.js +89 -0
  182. package/dist/explore/cli/feature.js.map +1 -0
  183. package/dist/explore/cli/file.d.ts +12 -0
  184. package/dist/explore/cli/file.d.ts.map +1 -0
  185. package/dist/explore/cli/file.js +139 -0
  186. package/dist/explore/cli/file.js.map +1 -0
  187. package/dist/explore/cli/hot-files.d.ts +11 -0
  188. package/dist/explore/cli/hot-files.d.ts.map +1 -0
  189. package/dist/explore/cli/hot-files.js +63 -0
  190. package/dist/explore/cli/hot-files.js.map +1 -0
  191. package/dist/explore/context-hook.d.ts +42 -0
  192. package/dist/explore/context-hook.d.ts.map +1 -0
  193. package/dist/explore/context-hook.js +131 -0
  194. package/dist/explore/context-hook.js.map +1 -0
  195. package/dist/explore/finding-context.d.ts +69 -0
  196. package/dist/explore/finding-context.d.ts.map +1 -0
  197. package/dist/explore/finding-context.js +102 -0
  198. package/dist/explore/finding-context.js.map +1 -0
  199. package/dist/explore/format.d.ts +64 -0
  200. package/dist/explore/format.d.ts.map +1 -0
  201. package/dist/explore/format.js +99 -0
  202. package/dist/explore/format.js.map +1 -0
  203. package/dist/explore/load.d.ts +50 -0
  204. package/dist/explore/load.d.ts.map +1 -0
  205. package/dist/explore/load.js +197 -0
  206. package/dist/explore/load.js.map +1 -0
  207. package/dist/explore/queries.d.ts +413 -0
  208. package/dist/explore/queries.d.ts.map +1 -0
  209. package/dist/explore/queries.js +855 -0
  210. package/dist/explore/queries.js.map +1 -0
  211. package/dist/explore/types.d.ts +130 -0
  212. package/dist/explore/types.d.ts.map +1 -0
  213. package/dist/explore/types.js +28 -0
  214. package/dist/explore/types.js.map +1 -0
  215. package/dist/explore-cli.d.ts +45 -0
  216. package/dist/explore-cli.d.ts.map +1 -0
  217. package/dist/explore-cli.js +213 -0
  218. package/dist/explore-cli.js.map +1 -0
  219. package/dist/generator.d.ts.map +1 -1
  220. package/dist/generator.js +19 -0
  221. package/dist/generator.js.map +1 -1
  222. package/dist/issue-cli.d.ts +62 -0
  223. package/dist/issue-cli.d.ts.map +1 -0
  224. package/dist/issue-cli.js +252 -0
  225. package/dist/issue-cli.js.map +1 -0
  226. package/dist/languages/csharp.d.ts.map +1 -1
  227. package/dist/languages/csharp.js +32 -11
  228. package/dist/languages/csharp.js.map +1 -1
  229. package/dist/languages/go.d.ts.map +1 -1
  230. package/dist/languages/go.js +5 -0
  231. package/dist/languages/go.js.map +1 -1
  232. package/dist/languages/index.d.ts +27 -0
  233. package/dist/languages/index.d.ts.map +1 -1
  234. package/dist/languages/index.js +35 -0
  235. package/dist/languages/index.js.map +1 -1
  236. package/dist/languages/java.d.ts.map +1 -1
  237. package/dist/languages/java.js +5 -0
  238. package/dist/languages/java.js.map +1 -1
  239. package/dist/languages/kotlin.d.ts.map +1 -1
  240. package/dist/languages/kotlin.js +5 -0
  241. package/dist/languages/kotlin.js.map +1 -1
  242. package/dist/languages/python.d.ts.map +1 -1
  243. package/dist/languages/python.js +5 -0
  244. package/dist/languages/python.js.map +1 -1
  245. package/dist/languages/ruby.d.ts.map +1 -1
  246. package/dist/languages/ruby.js +5 -0
  247. package/dist/languages/ruby.js.map +1 -1
  248. package/dist/languages/rust.d.ts.map +1 -1
  249. package/dist/languages/rust.js +5 -0
  250. package/dist/languages/rust.js.map +1 -1
  251. package/dist/languages/types.d.ts +79 -0
  252. package/dist/languages/types.d.ts.map +1 -1
  253. package/dist/languages/typescript.d.ts.map +1 -1
  254. package/dist/languages/typescript.js +6 -1
  255. package/dist/languages/typescript.js.map +1 -1
  256. package/package.json +2 -1
  257. package/templates/.claude/skills/dxkit-action/SKILL.md +126 -12
  258. package/templates/.claude/skills/dxkit-onboard/SKILL.md +31 -3
  259. package/templates/.claude/skills/dxkit-reports/SKILL.md +3 -1
  260. package/templates/AGENTS.md.template +8 -1
  261. package/dist/baseline/producers/licenses.d.ts +0 -23
  262. package/dist/baseline/producers/licenses.d.ts.map +0 -1
  263. package/dist/baseline/producers/licenses.js +0 -46
  264. package/dist/baseline/producers/licenses.js.map +0 -1
@@ -0,0 +1,855 @@
1
+ "use strict";
2
+ /**
3
+ * Canonical graph query module. Per CLAUDE.md Rule 12, every consumer
4
+ * (explore CLI subcommands, dashboard viz adapter, future 2.8 context
5
+ * CLI, future 2.8 reachability) imports from here — never reimplements
6
+ * graph traversal. Arch-check enforces.
7
+ *
8
+ * Sprint 1 ships the SKELETON: type signatures + empty implementations
9
+ * so the canonical entry points exist for the arch rule to lock onto.
10
+ * Sprint 2 fills the bodies as the explore CLI subcommands land.
11
+ *
12
+ * Every query is a pure function: takes a `Graph` (and optionally
13
+ * other args), returns a typed result. No side effects, no I/O,
14
+ * no caching — caching belongs at the loader level, not the query
15
+ * level.
16
+ */
17
+ Object.defineProperty(exports, "__esModule", { value: true });
18
+ exports.callersOf = callersOf;
19
+ exports.calleesOf = calleesOf;
20
+ exports.nodesInFile = nodesInFile;
21
+ exports.hotFilesQuery = hotFilesQuery;
22
+ exports.communitiesQuery = communitiesQuery;
23
+ exports.fileSummaryQuery = fileSummaryQuery;
24
+ exports.entryPointsQuery = entryPointsQuery;
25
+ exports.apiSurfaceQuery = apiSurfaceQuery;
26
+ exports.featureQuery = featureQuery;
27
+ exports.contextQuery = contextQuery;
28
+ exports.findingContextQuery = findingContextQuery;
29
+ // ─── Low-level primitives ────────────────────────────────────────────────────
30
+ /** Nodes that call into the given nodeId (predecessors via `calls` edges). */
31
+ function callersOf(graph, nodeId) {
32
+ const incoming = graph.edgesToNode.get(nodeId) ?? [];
33
+ const out = [];
34
+ for (const e of incoming) {
35
+ if (e.relation !== 'calls')
36
+ continue;
37
+ const n = graph.nodeById.get(e.from);
38
+ if (n)
39
+ out.push(n);
40
+ }
41
+ return out;
42
+ }
43
+ /** Nodes that the given nodeId calls into (successors via `calls` edges). */
44
+ function calleesOf(graph, nodeId) {
45
+ const outgoing = graph.edgesFromNode.get(nodeId) ?? [];
46
+ const out = [];
47
+ for (const e of outgoing) {
48
+ if (e.relation !== 'calls')
49
+ continue;
50
+ const n = graph.nodeById.get(e.to);
51
+ if (n)
52
+ out.push(n);
53
+ }
54
+ return out;
55
+ }
56
+ /** All nodes declared in the given source file. */
57
+ function nodesInFile(graph, sourceFile) {
58
+ return [...(graph.nodesByFile.get(sourceFile) ?? [])];
59
+ }
60
+ /**
61
+ * Top-N files by total in-degree (callers + importers). The
62
+ * "centrality" proxy — files many other files depend on. Useful as
63
+ * a "what's the foundational layer of this repo?" answer.
64
+ *
65
+ * Files are derived from the union of `sourceFile` across all nodes;
66
+ * the per-file aggregation traverses each node's inbound/outbound
67
+ * edges. Limit defaults to 20 per the Sprint 0 spec.
68
+ */
69
+ function hotFilesQuery(graph, limit = 20) {
70
+ const perFile = new Map();
71
+ for (const node of graph.nodes) {
72
+ if (!node.sourceFile)
73
+ continue;
74
+ let agg = perFile.get(node.sourceFile);
75
+ if (!agg) {
76
+ agg = { callsIn: 0, callsOut: 0, nodes: [] };
77
+ perFile.set(node.sourceFile, agg);
78
+ }
79
+ agg.nodes.push(node);
80
+ for (const e of graph.edgesToNode.get(node.id) ?? []) {
81
+ if (e.relation === 'calls')
82
+ agg.callsIn++;
83
+ }
84
+ for (const e of graph.edgesFromNode.get(node.id) ?? []) {
85
+ if (e.relation === 'calls')
86
+ agg.callsOut++;
87
+ }
88
+ }
89
+ // Imports-in: count edges into the FILE's module node. Module
90
+ // nodes have `kind === 'module'` and their `sourceFile` IS the
91
+ // file path. Aggregate to the file by matching on that.
92
+ const importsInByFile = new Map();
93
+ for (const node of graph.nodes) {
94
+ if (node.kind !== 'module' || !node.sourceFile)
95
+ continue;
96
+ let count = 0;
97
+ for (const e of graph.edgesToNode.get(node.id) ?? []) {
98
+ if (e.relation === 'imports_from')
99
+ count++;
100
+ }
101
+ importsInByFile.set(node.sourceFile, (importsInByFile.get(node.sourceFile) ?? 0) + count);
102
+ }
103
+ const results = [];
104
+ for (const [sourceFile, agg] of perFile) {
105
+ const importsIn = importsInByFile.get(sourceFile) ?? 0;
106
+ // Pick a community via any of the file's nodes — module node
107
+ // first if present, else any symbol's community.
108
+ const moduleNode = agg.nodes.find((n) => n.kind === 'module');
109
+ const sampleNode = moduleNode ?? agg.nodes[0];
110
+ const community = sampleNode ? graph.communityByNode.get(sampleNode.id) : undefined;
111
+ results.push({
112
+ sourceFile,
113
+ callsIn: agg.callsIn,
114
+ importsIn,
115
+ callsOut: agg.callsOut,
116
+ communityId: community?.id,
117
+ communityLabel: community?.dominantSourceDir || undefined,
118
+ });
119
+ }
120
+ // Rank by total in-degree (calls + imports). Ties broken by
121
+ // alphabetical source file path for stable output.
122
+ results.sort((a, b) => {
123
+ const ai = a.callsIn + a.importsIn;
124
+ const bi = b.callsIn + b.importsIn;
125
+ if (bi !== ai)
126
+ return bi - ai;
127
+ return a.sourceFile.localeCompare(b.sourceFile);
128
+ });
129
+ return results.slice(0, limit);
130
+ }
131
+ /**
132
+ * Top-N communities by node count, with each community's top-3 hot
133
+ * files (by in-degree within the community). Gives a "what are the
134
+ * natural modules in this repo?" answer that complements `hot-files`
135
+ * (which is global).
136
+ */
137
+ function communitiesQuery(graph, limit = 8) {
138
+ const callsInByNode = computeCallsInByNode(graph);
139
+ const sortedCommunities = [...graph.communities].sort((a, b) => b.nodeIds.length - a.nodeIds.length);
140
+ return sortedCommunities.slice(0, limit).map((c) => {
141
+ // Per-file in-degree within this community only.
142
+ const inDegByFile = new Map();
143
+ for (const nid of c.nodeIds) {
144
+ const node = graph.nodeById.get(nid);
145
+ if (!node?.sourceFile)
146
+ continue;
147
+ const d = callsInByNode.get(nid) ?? 0;
148
+ inDegByFile.set(node.sourceFile, (inDegByFile.get(node.sourceFile) ?? 0) + d);
149
+ }
150
+ const topHotFiles = [...inDegByFile.entries()]
151
+ .filter(([, d]) => d > 0)
152
+ .sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]))
153
+ .slice(0, 3)
154
+ .map(([f]) => f);
155
+ return {
156
+ id: c.id,
157
+ nodeCount: c.nodeIds.length,
158
+ dominantSourceDir: c.dominantSourceDir,
159
+ dominantPack: c.dominantPack,
160
+ cohesion: c.cohesion,
161
+ topHotFiles,
162
+ };
163
+ });
164
+ }
165
+ /**
166
+ * Internal helper: precompute per-node call in-degree. Shared between
167
+ * `hotFilesQuery` (file-level aggregation) and `communitiesQuery`
168
+ * (community-bounded ranking). Per CLAUDE.md Rule 2 — one source of
169
+ * truth for the calls-in-degree counter.
170
+ */
171
+ function computeCallsInByNode(graph) {
172
+ const m = new Map();
173
+ for (const node of graph.nodes) {
174
+ let d = 0;
175
+ for (const e of graph.edgesToNode.get(node.id) ?? []) {
176
+ if (e.relation === 'calls')
177
+ d++;
178
+ }
179
+ if (d > 0)
180
+ m.set(node.id, d);
181
+ }
182
+ return m;
183
+ }
184
+ /**
185
+ * Build the per-file summary. `found: false` when the file isn't in
186
+ * the graph (excluded by minified detection / vendored / unsupported
187
+ * extension); the consumer handles that case with an explanatory
188
+ * note instead of an empty result.
189
+ */
190
+ function fileSummaryQuery(graph, sourceFile) {
191
+ const nodes = nodesInFile(graph, sourceFile);
192
+ if (nodes.length === 0) {
193
+ return {
194
+ sourceFile,
195
+ found: false,
196
+ symbols: [],
197
+ callerFiles: [],
198
+ calleeFiles: [],
199
+ importsIn: [],
200
+ importsOut: [],
201
+ };
202
+ }
203
+ // Per-symbol summary for non-module nodes.
204
+ const symbols = [];
205
+ // Caller / callee aggregation, deduped at the file level.
206
+ const callerCounts = new Map();
207
+ const calleeCounts = new Map();
208
+ // Imports in/out aggregation against the file's module node.
209
+ const importsInFiles = new Set();
210
+ const importsOutFiles = new Set();
211
+ for (const node of nodes) {
212
+ if (node.kind !== 'module') {
213
+ let inCalls = 0;
214
+ let outCalls = 0;
215
+ for (const e of graph.edgesToNode.get(node.id) ?? []) {
216
+ if (e.relation === 'calls') {
217
+ inCalls++;
218
+ const src = graph.nodeById.get(e.from);
219
+ if (src?.sourceFile && src.sourceFile !== sourceFile) {
220
+ callerCounts.set(src.sourceFile, (callerCounts.get(src.sourceFile) ?? 0) + 1);
221
+ }
222
+ }
223
+ }
224
+ for (const e of graph.edgesFromNode.get(node.id) ?? []) {
225
+ if (e.relation === 'calls') {
226
+ outCalls++;
227
+ const dst = graph.nodeById.get(e.to);
228
+ if (dst?.sourceFile && dst.sourceFile !== sourceFile) {
229
+ calleeCounts.set(dst.sourceFile, (calleeCounts.get(dst.sourceFile) ?? 0) + 1);
230
+ }
231
+ }
232
+ }
233
+ symbols.push({
234
+ id: node.id,
235
+ kind: node.kind,
236
+ label: node.label,
237
+ line: node.line,
238
+ exported: node.exported,
239
+ callsIn: inCalls,
240
+ callsOut: outCalls,
241
+ });
242
+ }
243
+ else {
244
+ // Module node: harvest imports edges.
245
+ for (const e of graph.edgesToNode.get(node.id) ?? []) {
246
+ if (e.relation === 'imports_from') {
247
+ const src = graph.nodeById.get(e.from);
248
+ if (src?.sourceFile && src.sourceFile !== sourceFile) {
249
+ importsInFiles.add(src.sourceFile);
250
+ }
251
+ }
252
+ }
253
+ for (const e of graph.edgesFromNode.get(node.id) ?? []) {
254
+ if (e.relation === 'imports_from') {
255
+ const dst = graph.nodeById.get(e.to);
256
+ if (dst?.sourceFile && dst.sourceFile !== sourceFile) {
257
+ importsOutFiles.add(dst.sourceFile);
258
+ }
259
+ }
260
+ }
261
+ }
262
+ }
263
+ // Pick a representative node for community lookup — module first,
264
+ // else any symbol.
265
+ const moduleNode = nodes.find((n) => n.kind === 'module');
266
+ const sampleNode = moduleNode ?? nodes[0];
267
+ const community = sampleNode ? graph.communityByNode.get(sampleNode.id) : undefined;
268
+ return {
269
+ sourceFile,
270
+ found: true,
271
+ symbols: symbols.sort((a, b) => b.callsIn - a.callsIn || a.label.localeCompare(b.label)),
272
+ callerFiles: [...callerCounts.entries()]
273
+ .map(([sourceFile, count]) => ({ sourceFile, count }))
274
+ .sort((a, b) => b.count - a.count || a.sourceFile.localeCompare(b.sourceFile)),
275
+ calleeFiles: [...calleeCounts.entries()]
276
+ .map(([sourceFile, count]) => ({ sourceFile, count }))
277
+ .sort((a, b) => b.count - a.count || a.sourceFile.localeCompare(b.sourceFile)),
278
+ importsIn: [...importsInFiles].sort().map((sourceFile) => ({ sourceFile })),
279
+ importsOut: [...importsOutFiles].sort().map((sourceFile) => ({ sourceFile })),
280
+ communityId: community?.id,
281
+ communityLabel: community?.dominantSourceDir || undefined,
282
+ communityPack: community?.dominantPack || undefined,
283
+ };
284
+ }
285
+ /**
286
+ * Discover entry-point symbols by intersecting graph nodes with the
287
+ * union of active packs' `primaryComponentPaths` + `routePaths`. The
288
+ * rank is by call out-degree — entry points typically fan OUT (they
289
+ * receive a request, then call many downstream functions). A high
290
+ * out-degree node in a primary-architecture path is almost certainly
291
+ * a real entry point.
292
+ *
293
+ * `flags` is the per-pack boolean map from `DetectedStack.languages`;
294
+ * only patterns from active packs contribute. This matches the
295
+ * existing pack-driven analyzer pattern.
296
+ */
297
+ function entryPointsQuery(graph, primaryPaths, routePaths, limit = 10) {
298
+ if (primaryPaths.length === 0 && routePaths.length === 0) {
299
+ return [];
300
+ }
301
+ // Classify each source file by whether it matches a pattern.
302
+ // Patterns are case-insensitive substrings of the relative POSIX
303
+ // path (per the architecturalShape contract). routePaths overlap
304
+ // primaryComponentPaths in many packs; tag a file by the most
305
+ // specific match (route > primary > none).
306
+ const classify = (sourceFile) => {
307
+ const lower = sourceFile.toLowerCase();
308
+ for (const p of routePaths) {
309
+ if (lower.includes(p.toLowerCase())) {
310
+ return { matched: true, label: patternLabel(p), isRoute: true };
311
+ }
312
+ }
313
+ for (const p of primaryPaths) {
314
+ if (lower.includes(p.toLowerCase())) {
315
+ return { matched: true, label: patternLabel(p), isRoute: false };
316
+ }
317
+ }
318
+ return { matched: false, label: '', isRoute: false };
319
+ };
320
+ const results = [];
321
+ for (const node of graph.nodes) {
322
+ if (node.kind === 'module')
323
+ continue;
324
+ if (!node.sourceFile)
325
+ continue;
326
+ const c = classify(node.sourceFile);
327
+ if (!c.matched)
328
+ continue;
329
+ let callsOut = 0;
330
+ for (const e of graph.edgesFromNode.get(node.id) ?? []) {
331
+ if (e.relation === 'calls')
332
+ callsOut++;
333
+ }
334
+ if (callsOut === 0)
335
+ continue; // entry points fan out; zero-out-degree symbols aren't entry points
336
+ // Pack: derive from extension via the helper below. Avoids a
337
+ // second pack registry import — keeps queries.ts independent of
338
+ // languages/index.ts for this lookup.
339
+ results.push({
340
+ sourceFile: node.sourceFile,
341
+ line: node.line,
342
+ symbol: node.label,
343
+ componentType: c.label,
344
+ callsOut,
345
+ pack: packFromExt(node.sourceFile),
346
+ });
347
+ }
348
+ // Rank by callsOut desc, ties by sourceFile asc for stability.
349
+ results.sort((a, b) => b.callsOut - a.callsOut || a.sourceFile.localeCompare(b.sourceFile));
350
+ return results.slice(0, limit);
351
+ }
352
+ /**
353
+ * Extract a human-readable label from a path pattern. E.g.
354
+ * `/controllers/` → `controllers`, `/Forms/` → `forms`.
355
+ */
356
+ function patternLabel(pattern) {
357
+ return pattern.replace(/[/\\]/g, '').toLowerCase();
358
+ }
359
+ // Per-extension pack id derivation. Mirrors EXT_TO_PACK in the
360
+ // Python script. Kept here as a private helper rather than imported
361
+ // from the registry to keep queries.ts importable without pulling
362
+ // the whole pack module surface.
363
+ function packFromExt(sourceFile) {
364
+ const i = sourceFile.lastIndexOf('.');
365
+ if (i < 0)
366
+ return '';
367
+ const ext = sourceFile.slice(i).toLowerCase();
368
+ const map = {
369
+ '.ts': 'typescript',
370
+ '.tsx': 'typescript',
371
+ '.js': 'typescript',
372
+ '.jsx': 'typescript',
373
+ '.mjs': 'typescript',
374
+ '.cjs': 'typescript',
375
+ '.py': 'python',
376
+ '.go': 'go',
377
+ '.rs': 'rust',
378
+ '.cs': 'csharp',
379
+ '.kt': 'kotlin',
380
+ '.kts': 'kotlin',
381
+ '.java': 'java',
382
+ '.rb': 'ruby',
383
+ };
384
+ return map[ext] ?? '';
385
+ }
386
+ /**
387
+ * Find exported symbols with zero internal callers. `packsExcluded`
388
+ * lists pack ids whose `exportDetection.reliability === 'unreliable'`
389
+ * — those packs' nodes are skipped because we can't trust their
390
+ * `exported` flag (today: ruby). The consumer surfaces the exclusion
391
+ * as a note in its output.
392
+ */
393
+ function apiSurfaceQuery(graph, packsExcluded, limit = 25) {
394
+ const excluded = new Set(packsExcluded);
395
+ const results = [];
396
+ for (const node of graph.nodes) {
397
+ if (node.kind === 'module')
398
+ continue;
399
+ if (node.exported !== true)
400
+ continue; // absent or false → skip
401
+ const pack = packFromExt(node.sourceFile);
402
+ if (excluded.has(pack))
403
+ continue;
404
+ // Zero internal callers — check inbound calls edges. Note: the
405
+ // calls in-degree includes potential graphify same-name conflicts
406
+ // (run() at one site can attract calls meant for run() at another),
407
+ // so this is a "best effort" — but consumers know that's the limit.
408
+ let hasCaller = false;
409
+ for (const e of graph.edgesToNode.get(node.id) ?? []) {
410
+ if (e.relation === 'calls') {
411
+ hasCaller = true;
412
+ break;
413
+ }
414
+ }
415
+ if (hasCaller)
416
+ continue;
417
+ results.push({
418
+ sourceFile: node.sourceFile,
419
+ line: node.line,
420
+ symbol: node.label,
421
+ kind: node.kind,
422
+ pack,
423
+ });
424
+ }
425
+ // Sort by sourceFile asc (groups by file naturally) then line asc.
426
+ results.sort((a, b) => a.sourceFile.localeCompare(b.sourceFile) || (a.line ?? 0) - (b.line ?? 0));
427
+ return results.slice(0, limit);
428
+ }
429
+ /**
430
+ * The marquee query — "where is feature X implemented?" Three-stage
431
+ * resolution:
432
+ *
433
+ * 1. Direct symbolIndex lookup (case-insensitive, exact match on
434
+ * the stripped name)
435
+ * 2. Substring expansion (opt-in via opts.substring) — scans every
436
+ * node's label for substring match
437
+ * 3. Structural expansion — for each seed, gather community
438
+ * membership + immediate callers + callees, group by community
439
+ *
440
+ * On zero hits, computes edit-distance suggestions against the
441
+ * symbolIndex keys so the caller can prompt the user with "did you
442
+ * mean..."
443
+ */
444
+ function featureQuery(graph, keyword, opts = {}) {
445
+ const limit = opts.limit ?? 50;
446
+ const kw = keyword.toLowerCase().trim();
447
+ if (!kw) {
448
+ return { results: [], suggestions: [] };
449
+ }
450
+ // Stage 1 + 2: direct symbolIndex match + optional substring expansion.
451
+ const seedIds = findSeedIds(graph, kw, opts.substring ?? false);
452
+ if (seedIds.size === 0) {
453
+ return { results: [], suggestions: suggestionsFor(graph, kw) };
454
+ }
455
+ // Stage 3: structural expansion. For each seed, gather its
456
+ // community + direct callers/callees. Group expanded set by
457
+ // community id.
458
+ const expandedByComm = new Map();
459
+ const unclusteredExpansion = new Set();
460
+ for (const seedId of seedIds) {
461
+ const community = graph.communityByNode.get(seedId);
462
+ const bucket = community
463
+ ? (expandedByComm.get(community.id) ?? new Set())
464
+ : unclusteredExpansion;
465
+ bucket.add(seedId);
466
+ // Direct callers (1 hop)
467
+ for (const e of graph.edgesToNode.get(seedId) ?? []) {
468
+ if (e.relation === 'calls')
469
+ bucket.add(e.from);
470
+ }
471
+ // Direct callees (1 hop)
472
+ for (const e of graph.edgesFromNode.get(seedId) ?? []) {
473
+ if (e.relation === 'calls')
474
+ bucket.add(e.to);
475
+ }
476
+ if (community)
477
+ expandedByComm.set(community.id, bucket);
478
+ }
479
+ // Build cluster objects, ranked by seed count then size.
480
+ const clusters = [];
481
+ let clusterIdx = 0;
482
+ const buildCluster = (nodeIds, community) => {
483
+ const files = new Set();
484
+ const keySymbols = new Set();
485
+ let seedHits = 0;
486
+ for (const nid of nodeIds) {
487
+ const node = graph.nodeById.get(nid);
488
+ if (!node)
489
+ continue;
490
+ if (node.sourceFile)
491
+ files.add(node.sourceFile);
492
+ if (seedIds.has(nid)) {
493
+ seedHits++;
494
+ // Promote seed nodes to keySymbols list.
495
+ const stripped = stripParens(node.label);
496
+ if (stripped)
497
+ keySymbols.add(stripped);
498
+ }
499
+ }
500
+ // Top-8 key symbols by alpha for stable output.
501
+ const keySymbolsList = [...keySymbols].sort().slice(0, 8);
502
+ const filesList = [...files].sort();
503
+ const role = roleLabel(community);
504
+ return {
505
+ clusterId: clusterIdx++,
506
+ communityId: community?.id,
507
+ role,
508
+ dominantSourceDir: community?.dominantSourceDir ?? '',
509
+ files: filesList,
510
+ keySymbols: keySymbolsList,
511
+ seedHits,
512
+ };
513
+ };
514
+ for (const [commId, ids] of expandedByComm) {
515
+ const community = graph.communityById.get(commId);
516
+ clusters.push(buildCluster(ids, community));
517
+ }
518
+ if (unclusteredExpansion.size > 0) {
519
+ clusters.push(buildCluster(unclusteredExpansion, undefined));
520
+ }
521
+ // Rank clusters by seedHits desc, then size desc, then community id asc.
522
+ clusters.sort((a, b) => b.seedHits - a.seedHits ||
523
+ b.files.length - a.files.length ||
524
+ (a.communityId ?? 9999) - (b.communityId ?? 9999));
525
+ const limitedClusters = clusters.slice(0, limit);
526
+ // Central entry point: across all seed ids, the one with the
527
+ // highest call in-degree globally.
528
+ let centralId;
529
+ let centralCount = 0;
530
+ for (const seedId of seedIds) {
531
+ let inDeg = 0;
532
+ for (const e of graph.edgesToNode.get(seedId) ?? []) {
533
+ if (e.relation === 'calls')
534
+ inDeg++;
535
+ }
536
+ if (inDeg > centralCount) {
537
+ centralCount = inDeg;
538
+ centralId = seedId;
539
+ }
540
+ }
541
+ let centralEntryPoint;
542
+ if (centralId && centralCount > 0) {
543
+ const n = graph.nodeById.get(centralId);
544
+ if (n) {
545
+ centralEntryPoint = {
546
+ sourceFile: n.sourceFile,
547
+ line: n.line,
548
+ symbol: n.label,
549
+ calledFrom: centralCount,
550
+ };
551
+ }
552
+ }
553
+ return { results: limitedClusters, suggestions: [], centralEntryPoint };
554
+ }
555
+ /**
556
+ * The marquee token-reduction primitive — "give me just the relevant
557
+ * structural slice for this query." Resolves seeds the same way
558
+ * `featureQuery` does (shared `findSeedIds`), then expands breadth-
559
+ * first through `calls` edges, stopping when the running token
560
+ * estimate fills the budget (or an optional `maxDepth` ceiling is
561
+ * reached). Adaptive depth falls out for free: a hot symbol's
562
+ * immediate neighbors fill the budget at hop 1, while a cold symbol's
563
+ * sparse neighborhood leaves room to reach hop 2+.
564
+ *
565
+ * Pure: no I/O, no formatting. Same `Graph` in → same `ContextResult`
566
+ * out.
567
+ */
568
+ function contextQuery(graph, keyword, opts = {}) {
569
+ const budget = opts.budget ?? 2000;
570
+ const tokensPerNode = opts.tokensPerNode ?? 15;
571
+ const maxDepth = opts.maxDepth ?? Infinity;
572
+ const kw = keyword.toLowerCase().trim();
573
+ const empty = {
574
+ query: keyword,
575
+ matched: false,
576
+ selection: [],
577
+ byCommunity: [],
578
+ blastRadius: { callers: 0, callerFiles: 0 },
579
+ truncated: false,
580
+ omittedCount: 0,
581
+ estimatedTokens: 0,
582
+ budget,
583
+ suggestions: [],
584
+ };
585
+ if (!kw)
586
+ return empty;
587
+ const seedIds = findSeedIds(graph, kw, opts.substring ?? false);
588
+ if (seedIds.size === 0) {
589
+ return { ...empty, suggestions: suggestionsFor(graph, kw) };
590
+ }
591
+ // Budget-bounded BFS over `calls` edges. The queue carries (id, hop);
592
+ // seeds enter at hop 0. We add a node to the selection only if its
593
+ // estimated render cost still fits the budget; the first node that
594
+ // would overflow flips `truncated` and we drain the remaining queue
595
+ // into `omittedCount` (a lower bound — honest "+N more").
596
+ const selection = [];
597
+ const visited = new Set();
598
+ const queue = [];
599
+ for (const id of seedIds)
600
+ queue.push({ id, hop: 0 });
601
+ let estimatedTokens = 0;
602
+ let truncated = false;
603
+ let omittedCount = 0;
604
+ while (queue.length > 0) {
605
+ const { id, hop } = queue.shift();
606
+ if (visited.has(id))
607
+ continue;
608
+ visited.add(id);
609
+ const node = graph.nodeById.get(id);
610
+ if (!node || node.kind === 'module')
611
+ continue;
612
+ if (estimatedTokens + tokensPerNode > budget) {
613
+ // Budget exhausted — this node + everything still queued is omitted.
614
+ truncated = true;
615
+ omittedCount++;
616
+ continue;
617
+ }
618
+ const callers = callersOf(graph, id);
619
+ const callees = calleesOf(graph, id);
620
+ selection.push({
621
+ id,
622
+ symbol: stripParens(node.label),
623
+ sourceFile: node.sourceFile,
624
+ line: node.line,
625
+ kind: node.kind,
626
+ hop,
627
+ callsIn: callers.length,
628
+ callsOut: callees.length,
629
+ });
630
+ estimatedTokens += tokensPerNode;
631
+ if (hop < maxDepth) {
632
+ for (const n of callers)
633
+ if (!visited.has(n.id))
634
+ queue.push({ id: n.id, hop: hop + 1 });
635
+ for (const n of callees)
636
+ if (!visited.has(n.id))
637
+ queue.push({ id: n.id, hop: hop + 1 });
638
+ }
639
+ }
640
+ // Anchor: highest call-in-degree seed (the "start here" symbol).
641
+ let anchor;
642
+ let anchorCount = -1;
643
+ for (const seedId of seedIds) {
644
+ const node = graph.nodeById.get(seedId);
645
+ if (!node)
646
+ continue;
647
+ const inDeg = callersOf(graph, seedId).length;
648
+ if (inDeg > anchorCount) {
649
+ anchorCount = inDeg;
650
+ anchor = {
651
+ sourceFile: node.sourceFile,
652
+ line: node.line,
653
+ symbol: stripParens(node.label),
654
+ calledFrom: inDeg,
655
+ };
656
+ }
657
+ }
658
+ // Blast radius: unique callers of the SEEDS + distinct caller files
659
+ // (the surface a change to the matched symbols would touch).
660
+ const callerIds = new Set();
661
+ const callerFiles = new Set();
662
+ for (const seedId of seedIds) {
663
+ for (const caller of callersOf(graph, seedId)) {
664
+ callerIds.add(caller.id);
665
+ if (caller.sourceFile)
666
+ callerFiles.add(caller.sourceFile);
667
+ }
668
+ }
669
+ // Group the selection by community for orientation. Communities
670
+ // containing a SEED (hop-0) symbol rank first — the reader cares
671
+ // most about where the matched symbols live, not the biggest
672
+ // incidental cluster the BFS fanned into. (graphify often lumps
673
+ // much of a repo into one mega-community; sorting purely by size
674
+ // would bury the seed under that grab-bag.)
675
+ const groupsByComm = new Map();
676
+ for (const sel of selection) {
677
+ const community = graph.communityByNode.get(sel.id);
678
+ const key = community?.id;
679
+ let group = groupsByComm.get(key);
680
+ if (!group) {
681
+ group = {
682
+ communityId: community?.id,
683
+ role: roleLabel(community),
684
+ files: [],
685
+ symbols: [],
686
+ hasSeed: false,
687
+ };
688
+ groupsByComm.set(key, group);
689
+ }
690
+ if (sel.sourceFile && !group.files.includes(sel.sourceFile))
691
+ group.files.push(sel.sourceFile);
692
+ group.symbols.push(sel.symbol);
693
+ if (sel.hop === 0)
694
+ group.hasSeed = true;
695
+ }
696
+ const byCommunity = [...groupsByComm.values()]
697
+ .sort((a, b) => Number(b.hasSeed) - Number(a.hasSeed) ||
698
+ b.symbols.length - a.symbols.length ||
699
+ (a.communityId ?? 9999) - (b.communityId ?? 9999))
700
+ .map(({ hasSeed: _hasSeed, ...group }) => group);
701
+ return {
702
+ query: keyword,
703
+ matched: true,
704
+ anchor,
705
+ selection,
706
+ byCommunity,
707
+ blastRadius: { callers: callerIds.size, callerFiles: callerFiles.size },
708
+ truncated,
709
+ omittedCount,
710
+ estimatedTokens,
711
+ budget,
712
+ suggestions: [],
713
+ };
714
+ }
715
+ /**
716
+ * Graph context for one finding location. Reuses `fileSummaryQuery`
717
+ * for the file-level caller aggregation + community lookup (Rule 2 —
718
+ * one source of truth for "who depends on this file"), then maps an
719
+ * optional `line` to the nearest enclosing declaration.
720
+ *
721
+ * Pure: same `Graph` + location in → same `FindingContext` out. The
722
+ * enrichment adapter (`src/explore/finding-context.ts`) owns the
723
+ * graph load, the per-finding loop, and the dedupe budget.
724
+ */
725
+ function findingContextQuery(graph, sourceFile, line, opts = {}) {
726
+ const topN = opts.topCallerFiles ?? 5;
727
+ const summary = fileSummaryQuery(graph, sourceFile);
728
+ if (!summary.found) {
729
+ return {
730
+ found: false,
731
+ sourceFile,
732
+ blastRadius: { callerFiles: 0, callers: 0, topCallerFiles: [] },
733
+ };
734
+ }
735
+ const callers = summary.callerFiles.reduce((acc, c) => acc + c.count, 0);
736
+ const topCallerFiles = summary.callerFiles.slice(0, topN).map((c) => c.sourceFile);
737
+ let enclosingSymbol;
738
+ if (typeof line === 'number') {
739
+ let best;
740
+ for (const sym of summary.symbols) {
741
+ if (typeof sym.line !== 'number')
742
+ continue;
743
+ if (sym.line <= line && (best?.line === undefined || sym.line > best.line)) {
744
+ best = sym;
745
+ }
746
+ }
747
+ if (best)
748
+ enclosingSymbol = { symbol: stripParens(best.label), line: best.line };
749
+ }
750
+ const community = summary.communityId !== undefined
751
+ ? {
752
+ id: summary.communityId,
753
+ role: summary.communityLabel ?? `community-${summary.communityId}`,
754
+ }
755
+ : { role: 'unclustered' };
756
+ return {
757
+ found: true,
758
+ sourceFile,
759
+ community,
760
+ blastRadius: { callerFiles: summary.callerFiles.length, callers, topCallerFiles },
761
+ enclosingSymbol,
762
+ };
763
+ }
764
+ function roleLabel(community) {
765
+ if (!community)
766
+ return 'unclustered';
767
+ if (community.dominantSourceDir)
768
+ return community.dominantSourceDir;
769
+ return `community-${community.id}`;
770
+ }
771
+ function stripParens(label) {
772
+ if (!label)
773
+ return '';
774
+ let s = label.replace(/\(\)$/, '');
775
+ if (s.includes('.'))
776
+ s = s.split('.').pop() ?? s;
777
+ return s;
778
+ }
779
+ /**
780
+ * Iterative Levenshtein with O(min(a, b)) memory. Fast enough for
781
+ * the suggestions scan (a few hundred symbolIndex keys × ≤20 chars).
782
+ */
783
+ function levenshtein(a, b) {
784
+ if (a === b)
785
+ return 0;
786
+ if (a.length === 0)
787
+ return b.length;
788
+ if (b.length === 0)
789
+ return a.length;
790
+ const long = a.length >= b.length ? a : b;
791
+ const short = a.length >= b.length ? b : a;
792
+ let prev = new Array(short.length + 1);
793
+ let curr = new Array(short.length + 1);
794
+ for (let i = 0; i <= short.length; i++)
795
+ prev[i] = i;
796
+ for (let i = 1; i <= long.length; i++) {
797
+ curr[0] = i;
798
+ for (let j = 1; j <= short.length; j++) {
799
+ const cost = long[i - 1] === short[j - 1] ? 0 : 1;
800
+ curr[j] = Math.min(curr[j - 1] + 1, prev[j] + 1, prev[j - 1] + cost);
801
+ }
802
+ [prev, curr] = [curr, prev];
803
+ }
804
+ return prev[short.length];
805
+ }
806
+ /**
807
+ * Resolve seed node ids for a (lowercased, trimmed) keyword. Stage 1
808
+ * is an exact symbolIndex hit; stage 2 (opt-in) adds substring
809
+ * matches across the index keys. Shared by `featureQuery` and
810
+ * `contextQuery` so the two surfaces match identically — one source
811
+ * of truth for "what does this keyword resolve to" (Rule 2).
812
+ */
813
+ function findSeedIds(graph, kw, substring) {
814
+ const seedIds = new Set(graph.symbolIndex[kw] ?? []);
815
+ if (substring) {
816
+ for (const [key, ids] of Object.entries(graph.symbolIndex)) {
817
+ if (key.includes(kw)) {
818
+ for (const id of ids)
819
+ seedIds.add(id);
820
+ }
821
+ }
822
+ }
823
+ return seedIds;
824
+ }
825
+ /**
826
+ * "Did you mean" suggestions for a keyword that matched no symbols.
827
+ * Two merged flavors: substring matches (symbols whose name CONTAINS
828
+ * the keyword — the common case, since users rarely type exact long
829
+ * symbol names) and Levenshtein ≤2 typo candidates. Substring is only
830
+ * tried for keywords of length ≥ 3 (shorter generates too many false
831
+ * positives). Top-5 by hit count. Shared by `featureQuery` +
832
+ * `contextQuery`.
833
+ */
834
+ function suggestionsFor(graph, kw) {
835
+ const suggestions = [];
836
+ const seen = new Set();
837
+ if (kw.length >= 3) {
838
+ for (const key of Object.keys(graph.symbolIndex)) {
839
+ if (key.includes(kw)) {
840
+ suggestions.push({ key, hits: graph.symbolIndex[key].length });
841
+ seen.add(key);
842
+ }
843
+ }
844
+ }
845
+ for (const key of Object.keys(graph.symbolIndex)) {
846
+ if (seen.has(key))
847
+ continue;
848
+ if (levenshtein(kw, key) <= 2) {
849
+ suggestions.push({ key, hits: graph.symbolIndex[key].length });
850
+ }
851
+ }
852
+ suggestions.sort((a, b) => b.hits - a.hits || a.key.localeCompare(b.key));
853
+ return suggestions.slice(0, 5);
854
+ }
855
+ //# sourceMappingURL=queries.js.map