@optave/codegraph 3.1.4 → 3.2.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 (210) hide show
  1. package/README.md +29 -72
  2. package/package.json +10 -8
  3. package/src/ast-analysis/engine.js +260 -246
  4. package/src/ast-analysis/shared.js +2 -14
  5. package/src/ast-analysis/visitors/cfg-visitor.js +635 -649
  6. package/src/ast-analysis/visitors/complexity-visitor.js +135 -139
  7. package/src/ast-analysis/visitors/dataflow-visitor.js +230 -224
  8. package/src/cli/commands/ast.js +4 -7
  9. package/src/cli/commands/audit.js +11 -11
  10. package/src/cli/commands/batch.js +6 -5
  11. package/src/cli/commands/branch-compare.js +1 -1
  12. package/src/cli/commands/brief.js +12 -0
  13. package/src/cli/commands/build.js +1 -1
  14. package/src/cli/commands/cfg.js +5 -8
  15. package/src/cli/commands/check.js +28 -36
  16. package/src/cli/commands/children.js +9 -7
  17. package/src/cli/commands/co-change.js +5 -3
  18. package/src/cli/commands/communities.js +2 -6
  19. package/src/cli/commands/complexity.js +5 -3
  20. package/src/cli/commands/context.js +9 -8
  21. package/src/cli/commands/cycles.js +12 -8
  22. package/src/cli/commands/dataflow.js +5 -8
  23. package/src/cli/commands/deps.js +9 -8
  24. package/src/cli/commands/diff-impact.js +2 -6
  25. package/src/cli/commands/embed.js +1 -1
  26. package/src/cli/commands/export.js +34 -31
  27. package/src/cli/commands/exports.js +2 -6
  28. package/src/cli/commands/flow.js +5 -8
  29. package/src/cli/commands/fn-impact.js +9 -8
  30. package/src/cli/commands/impact.js +2 -6
  31. package/src/cli/commands/info.js +2 -2
  32. package/src/cli/commands/map.js +1 -1
  33. package/src/cli/commands/mcp.js +1 -1
  34. package/src/cli/commands/models.js +1 -1
  35. package/src/cli/commands/owners.js +5 -3
  36. package/src/cli/commands/path.js +2 -2
  37. package/src/cli/commands/plot.js +40 -31
  38. package/src/cli/commands/query.js +9 -8
  39. package/src/cli/commands/registry.js +2 -2
  40. package/src/cli/commands/roles.js +5 -8
  41. package/src/cli/commands/search.js +9 -3
  42. package/src/cli/commands/sequence.js +5 -8
  43. package/src/cli/commands/snapshot.js +6 -1
  44. package/src/cli/commands/stats.js +1 -1
  45. package/src/cli/commands/structure.js +5 -4
  46. package/src/cli/commands/triage.js +41 -30
  47. package/src/cli/commands/watch.js +1 -1
  48. package/src/cli/commands/where.js +2 -6
  49. package/src/cli/index.js +11 -5
  50. package/src/cli/shared/open-graph.js +13 -0
  51. package/src/cli/shared/options.js +22 -2
  52. package/src/cli.js +1 -1
  53. package/src/db/connection.js +140 -11
  54. package/src/{db.js → db/index.js} +12 -5
  55. package/src/db/migrations.js +42 -65
  56. package/src/db/query-builder.js +72 -9
  57. package/src/db/repository/base.js +1 -1
  58. package/src/db/repository/graph-read.js +3 -3
  59. package/src/db/repository/in-memory-repository.js +30 -28
  60. package/src/db/repository/nodes.js +10 -17
  61. package/src/domain/analysis/brief.js +155 -0
  62. package/src/domain/analysis/context.js +392 -0
  63. package/src/domain/analysis/dependencies.js +395 -0
  64. package/src/{analysis → domain/analysis}/exports.js +11 -6
  65. package/src/domain/analysis/impact.js +581 -0
  66. package/src/domain/analysis/module-map.js +348 -0
  67. package/src/{analysis → domain/analysis}/roles.js +12 -9
  68. package/src/{analysis → domain/analysis}/symbol-lookup.js +19 -11
  69. package/src/{builder → domain/graph/builder}/helpers.js +4 -4
  70. package/src/{builder → domain/graph/builder}/incremental.js +119 -93
  71. package/src/domain/graph/builder/pipeline.js +156 -0
  72. package/src/domain/graph/builder/stages/build-edges.js +376 -0
  73. package/src/{builder → domain/graph/builder}/stages/build-structure.js +4 -4
  74. package/src/{builder → domain/graph/builder}/stages/collect-files.js +2 -2
  75. package/src/{builder → domain/graph/builder}/stages/detect-changes.js +204 -183
  76. package/src/{builder → domain/graph/builder}/stages/finalize.js +4 -4
  77. package/src/domain/graph/builder/stages/insert-nodes.js +203 -0
  78. package/src/{builder → domain/graph/builder}/stages/parse-files.js +2 -2
  79. package/src/{builder → domain/graph/builder}/stages/resolve-imports.js +1 -1
  80. package/src/{builder → domain/graph/builder}/stages/run-analyses.js +2 -2
  81. package/src/{change-journal.js → domain/graph/change-journal.js} +1 -1
  82. package/src/{cycles.js → domain/graph/cycles.js} +4 -4
  83. package/src/{journal.js → domain/graph/journal.js} +1 -1
  84. package/src/{resolve.js → domain/graph/resolve.js} +2 -2
  85. package/src/{watcher.js → domain/graph/watcher.js} +7 -7
  86. package/src/{parser.js → domain/parser.js} +24 -15
  87. package/src/{queries.js → domain/queries.js} +17 -16
  88. package/src/{embeddings → domain/search}/generator.js +3 -3
  89. package/src/{embeddings → domain/search}/models.js +2 -2
  90. package/src/{embeddings → domain/search}/search/cli-formatter.js +1 -1
  91. package/src/{embeddings → domain/search}/search/filters.js +9 -5
  92. package/src/{embeddings → domain/search}/search/hybrid.js +1 -1
  93. package/src/{embeddings → domain/search}/search/keyword.js +13 -6
  94. package/src/{embeddings → domain/search}/search/prepare.js +15 -7
  95. package/src/{embeddings → domain/search}/search/semantic.js +1 -1
  96. package/src/{embeddings → domain/search}/strategies/structured.js +1 -1
  97. package/src/extractors/csharp.js +224 -207
  98. package/src/extractors/go.js +176 -172
  99. package/src/extractors/hcl.js +94 -78
  100. package/src/extractors/java.js +213 -207
  101. package/src/extractors/javascript.js +275 -305
  102. package/src/extractors/php.js +234 -221
  103. package/src/extractors/python.js +252 -250
  104. package/src/extractors/ruby.js +192 -185
  105. package/src/extractors/rust.js +182 -167
  106. package/src/{ast.js → features/ast.js} +13 -11
  107. package/src/{audit.js → features/audit.js} +20 -46
  108. package/src/{batch.js → features/batch.js} +5 -5
  109. package/src/{boundaries.js → features/boundaries.js} +100 -85
  110. package/src/{branch-compare.js → features/branch-compare.js} +3 -3
  111. package/src/{cfg.js → features/cfg.js} +141 -150
  112. package/src/{check.js → features/check.js} +13 -30
  113. package/src/{cochange.js → features/cochange.js} +5 -5
  114. package/src/{communities.js → features/communities.js} +72 -57
  115. package/src/{complexity.js → features/complexity.js} +154 -143
  116. package/src/{dataflow.js → features/dataflow.js} +155 -158
  117. package/src/{export.js → features/export.js} +6 -6
  118. package/src/{flow.js → features/flow.js} +4 -4
  119. package/src/{viewer.js → features/graph-enrichment.js} +8 -8
  120. package/src/{manifesto.js → features/manifesto.js} +15 -12
  121. package/src/{owners.js → features/owners.js} +6 -5
  122. package/src/features/sequence.js +300 -0
  123. package/src/features/shared/find-nodes.js +31 -0
  124. package/src/{snapshot.js → features/snapshot.js} +3 -3
  125. package/src/{structure.js → features/structure.js} +139 -108
  126. package/src/features/triage.js +141 -0
  127. package/src/graph/builders/dependency.js +33 -14
  128. package/src/graph/classifiers/risk.js +3 -2
  129. package/src/graph/classifiers/roles.js +6 -3
  130. package/src/index.cjs +16 -0
  131. package/src/index.js +40 -39
  132. package/src/{native.js → infrastructure/native.js} +1 -1
  133. package/src/mcp/middleware.js +1 -1
  134. package/src/mcp/server.js +68 -59
  135. package/src/mcp/tool-registry.js +15 -2
  136. package/src/mcp/tools/ast-query.js +1 -1
  137. package/src/mcp/tools/audit.js +1 -1
  138. package/src/mcp/tools/batch-query.js +1 -1
  139. package/src/mcp/tools/branch-compare.js +3 -1
  140. package/src/mcp/tools/brief.js +8 -0
  141. package/src/mcp/tools/cfg.js +1 -1
  142. package/src/mcp/tools/check.js +3 -3
  143. package/src/mcp/tools/co-changes.js +1 -1
  144. package/src/mcp/tools/code-owners.js +1 -1
  145. package/src/mcp/tools/communities.js +1 -1
  146. package/src/mcp/tools/complexity.js +1 -1
  147. package/src/mcp/tools/dataflow.js +2 -2
  148. package/src/mcp/tools/execution-flow.js +2 -2
  149. package/src/mcp/tools/export-graph.js +2 -2
  150. package/src/mcp/tools/find-cycles.js +2 -2
  151. package/src/mcp/tools/index.js +2 -0
  152. package/src/mcp/tools/list-repos.js +1 -1
  153. package/src/mcp/tools/sequence.js +1 -1
  154. package/src/mcp/tools/structure.js +1 -1
  155. package/src/mcp/tools/triage.js +2 -2
  156. package/src/{commands → presentation}/audit.js +2 -2
  157. package/src/{commands → presentation}/batch.js +1 -1
  158. package/src/{commands → presentation}/branch-compare.js +2 -2
  159. package/src/presentation/brief.js +51 -0
  160. package/src/{commands → presentation}/cfg.js +1 -1
  161. package/src/{commands → presentation}/check.js +2 -2
  162. package/src/{commands → presentation}/communities.js +1 -1
  163. package/src/{commands → presentation}/complexity.js +1 -1
  164. package/src/{commands → presentation}/dataflow.js +1 -1
  165. package/src/{commands → presentation}/flow.js +2 -2
  166. package/src/{commands → presentation}/manifesto.js +1 -1
  167. package/src/{commands → presentation}/owners.js +1 -1
  168. package/src/presentation/queries-cli/exports.js +53 -0
  169. package/src/presentation/queries-cli/impact.js +214 -0
  170. package/src/presentation/queries-cli/index.js +5 -0
  171. package/src/presentation/queries-cli/inspect.js +329 -0
  172. package/src/presentation/queries-cli/overview.js +196 -0
  173. package/src/presentation/queries-cli/path.js +65 -0
  174. package/src/presentation/queries-cli.js +27 -0
  175. package/src/{commands → presentation}/query.js +1 -1
  176. package/src/presentation/result-formatter.js +126 -3
  177. package/src/{commands → presentation}/sequence.js +2 -2
  178. package/src/{commands → presentation}/structure.js +1 -1
  179. package/src/presentation/table.js +0 -8
  180. package/src/{commands → presentation}/triage.js +1 -1
  181. package/src/{constants.js → shared/constants.js} +1 -1
  182. package/src/shared/file-utils.js +2 -2
  183. package/src/shared/generators.js +9 -5
  184. package/src/shared/hierarchy.js +1 -1
  185. package/src/{kinds.js → shared/kinds.js} +1 -1
  186. package/src/analysis/context.js +0 -408
  187. package/src/analysis/dependencies.js +0 -341
  188. package/src/analysis/impact.js +0 -463
  189. package/src/analysis/module-map.js +0 -322
  190. package/src/builder/pipeline.js +0 -130
  191. package/src/builder/stages/build-edges.js +0 -297
  192. package/src/builder/stages/insert-nodes.js +0 -195
  193. package/src/mcp.js +0 -2
  194. package/src/queries-cli.js +0 -866
  195. package/src/sequence.js +0 -289
  196. package/src/triage.js +0 -126
  197. /package/src/{builder → domain/graph/builder}/context.js +0 -0
  198. /package/src/{builder.js → domain/graph/builder.js} +0 -0
  199. /package/src/{embeddings → domain/search}/index.js +0 -0
  200. /package/src/{embeddings → domain/search}/stores/fts5.js +0 -0
  201. /package/src/{embeddings → domain/search}/stores/sqlite-blob.js +0 -0
  202. /package/src/{embeddings → domain/search}/strategies/source.js +0 -0
  203. /package/src/{embeddings → domain/search}/strategies/text-utils.js +0 -0
  204. /package/src/{config.js → infrastructure/config.js} +0 -0
  205. /package/src/{logger.js → infrastructure/logger.js} +0 -0
  206. /package/src/{registry.js → infrastructure/registry.js} +0 -0
  207. /package/src/{update-check.js → infrastructure/update-check.js} +0 -0
  208. /package/src/{commands → presentation}/cochange.js +0 -0
  209. /package/src/{errors.js → shared/errors.js} +0 -0
  210. /package/src/{paginate.js → shared/paginate.js} +0 -0
@@ -1,866 +0,0 @@
1
- /**
2
- * queries-cli.js — CLI display wrappers for query data functions.
3
- *
4
- * Each function calls its corresponding *Data() function from queries.js,
5
- * handles JSON/NDJSON output via outputResult(), then formats human-readable
6
- * output for the terminal.
7
- */
8
-
9
- import path from 'node:path';
10
- import { outputResult } from './infrastructure/result-formatter.js';
11
- import {
12
- childrenData,
13
- contextData,
14
- diffImpactData,
15
- diffImpactMermaid,
16
- explainData,
17
- exportsData,
18
- fileDepsData,
19
- fnDepsData,
20
- fnImpactData,
21
- impactAnalysisData,
22
- kindIcon,
23
- moduleMapData,
24
- pathData,
25
- queryNameData,
26
- rolesData,
27
- statsData,
28
- whereData,
29
- } from './queries.js';
30
-
31
- // ─── symbolPath ─────────────────────────────────────────────────────────
32
-
33
- export function symbolPath(from, to, customDbPath, opts = {}) {
34
- const data = pathData(from, to, customDbPath, opts);
35
- if (outputResult(data, null, opts)) return;
36
-
37
- if (data.error) {
38
- console.log(data.error);
39
- return;
40
- }
41
-
42
- if (!data.found) {
43
- const dir = data.reverse ? 'reverse ' : '';
44
- console.log(`No ${dir}path from "${from}" to "${to}" within ${data.maxDepth} hops.`);
45
- if (data.fromCandidates.length > 1) {
46
- console.log(
47
- `\n "${from}" matched ${data.fromCandidates.length} symbols — using top match: ${data.fromCandidates[0].name} (${data.fromCandidates[0].file}:${data.fromCandidates[0].line})`,
48
- );
49
- }
50
- if (data.toCandidates.length > 1) {
51
- console.log(
52
- ` "${to}" matched ${data.toCandidates.length} symbols — using top match: ${data.toCandidates[0].name} (${data.toCandidates[0].file}:${data.toCandidates[0].line})`,
53
- );
54
- }
55
- return;
56
- }
57
-
58
- if (data.hops === 0) {
59
- console.log(`\n"${from}" and "${to}" resolve to the same symbol (0 hops):`);
60
- const n = data.path[0];
61
- console.log(` ${kindIcon(n.kind)} ${n.name} (${n.kind}) -- ${n.file}:${n.line}\n`);
62
- return;
63
- }
64
-
65
- const dir = data.reverse ? ' (reverse)' : '';
66
- console.log(
67
- `\nPath from ${from} to ${to} (${data.hops} ${data.hops === 1 ? 'hop' : 'hops'})${dir}:\n`,
68
- );
69
- for (let i = 0; i < data.path.length; i++) {
70
- const n = data.path[i];
71
- const indent = ' '.repeat(i + 1);
72
- if (i === 0) {
73
- console.log(`${indent}${kindIcon(n.kind)} ${n.name} (${n.kind}) -- ${n.file}:${n.line}`);
74
- } else {
75
- console.log(
76
- `${indent}--[${n.edgeKind}]--> ${kindIcon(n.kind)} ${n.name} (${n.kind}) -- ${n.file}:${n.line}`,
77
- );
78
- }
79
- }
80
-
81
- if (data.alternateCount > 0) {
82
- console.log(
83
- `\n (${data.alternateCount} alternate shortest ${data.alternateCount === 1 ? 'path' : 'paths'} at same depth)`,
84
- );
85
- }
86
- console.log();
87
- }
88
-
89
- // ─── stats ──────────────────────────────────────────────────────────────
90
-
91
- export async function stats(customDbPath, opts = {}) {
92
- const data = statsData(customDbPath, { noTests: opts.noTests });
93
-
94
- // Community detection summary (async import for lazy-loading)
95
- try {
96
- const { communitySummaryForStats } = await import('./communities.js');
97
- data.communities = communitySummaryForStats(customDbPath, { noTests: opts.noTests });
98
- } catch {
99
- /* graphology may not be available */
100
- }
101
-
102
- if (outputResult(data, null, opts)) return;
103
-
104
- // Human-readable output
105
- console.log('\n# Codegraph Stats\n');
106
-
107
- // Nodes
108
- console.log(`Nodes: ${data.nodes.total} total`);
109
- const kindEntries = Object.entries(data.nodes.byKind).sort((a, b) => b[1] - a[1]);
110
- const kindParts = kindEntries.map(([k, v]) => `${k} ${v}`);
111
- for (let i = 0; i < kindParts.length; i += 3) {
112
- const row = kindParts
113
- .slice(i, i + 3)
114
- .map((p) => p.padEnd(18))
115
- .join('');
116
- console.log(` ${row}`);
117
- }
118
-
119
- // Edges
120
- console.log(`\nEdges: ${data.edges.total} total`);
121
- const edgeEntries = Object.entries(data.edges.byKind).sort((a, b) => b[1] - a[1]);
122
- const edgeParts = edgeEntries.map(([k, v]) => `${k} ${v}`);
123
- for (let i = 0; i < edgeParts.length; i += 3) {
124
- const row = edgeParts
125
- .slice(i, i + 3)
126
- .map((p) => p.padEnd(18))
127
- .join('');
128
- console.log(` ${row}`);
129
- }
130
-
131
- // Files
132
- console.log(`\nFiles: ${data.files.total} (${data.files.languages} languages)`);
133
- const langEntries = Object.entries(data.files.byLanguage).sort((a, b) => b[1] - a[1]);
134
- const langParts = langEntries.map(([k, v]) => `${k} ${v}`);
135
- for (let i = 0; i < langParts.length; i += 3) {
136
- const row = langParts
137
- .slice(i, i + 3)
138
- .map((p) => p.padEnd(18))
139
- .join('');
140
- console.log(` ${row}`);
141
- }
142
-
143
- // Cycles
144
- console.log(
145
- `\nCycles: ${data.cycles.fileLevel} file-level, ${data.cycles.functionLevel} function-level`,
146
- );
147
-
148
- // Hotspots
149
- if (data.hotspots.length > 0) {
150
- console.log(`\nTop ${data.hotspots.length} coupling hotspots:`);
151
- for (let i = 0; i < data.hotspots.length; i++) {
152
- const h = data.hotspots[i];
153
- console.log(
154
- ` ${String(i + 1).padStart(2)}. ${h.file.padEnd(35)} fan-in: ${String(h.fanIn).padStart(3)} fan-out: ${String(h.fanOut).padStart(3)}`,
155
- );
156
- }
157
- }
158
-
159
- // Embeddings
160
- if (data.embeddings) {
161
- const e = data.embeddings;
162
- console.log(
163
- `\nEmbeddings: ${e.count} vectors (${e.model || 'unknown'}, ${e.dim || '?'}d) built ${e.builtAt || 'unknown'}`,
164
- );
165
- } else {
166
- console.log('\nEmbeddings: not built');
167
- }
168
-
169
- // Quality
170
- if (data.quality) {
171
- const q = data.quality;
172
- const cc = q.callerCoverage;
173
- const cf = q.callConfidence;
174
- console.log(`\nGraph Quality: ${q.score}/100`);
175
- console.log(
176
- ` Caller coverage: ${(cc.ratio * 100).toFixed(1)}% (${cc.covered}/${cc.total} functions have >=1 caller)`,
177
- );
178
- console.log(
179
- ` Call confidence: ${(cf.ratio * 100).toFixed(1)}% (${cf.highConf}/${cf.total} call edges are high-confidence)`,
180
- );
181
- if (q.falsePositiveWarnings.length > 0) {
182
- console.log(' False-positive warnings:');
183
- for (const fp of q.falsePositiveWarnings) {
184
- console.log(` ! ${fp.name} (${fp.callerCount} callers) -- ${fp.file}:${fp.line}`);
185
- }
186
- }
187
- }
188
-
189
- // Roles
190
- if (data.roles && Object.keys(data.roles).length > 0) {
191
- const total = Object.values(data.roles).reduce((a, b) => a + b, 0);
192
- console.log(`\nRoles: ${total} classified symbols`);
193
- const roleParts = Object.entries(data.roles)
194
- .sort((a, b) => b[1] - a[1])
195
- .map(([k, v]) => `${k} ${v}`);
196
- for (let i = 0; i < roleParts.length; i += 3) {
197
- const row = roleParts
198
- .slice(i, i + 3)
199
- .map((p) => p.padEnd(18))
200
- .join('');
201
- console.log(` ${row}`);
202
- }
203
- }
204
-
205
- // Complexity
206
- if (data.complexity) {
207
- const cx = data.complexity;
208
- const miPart = cx.avgMI != null ? ` | avg MI: ${cx.avgMI} | min MI: ${cx.minMI}` : '';
209
- console.log(
210
- `\nComplexity: ${cx.analyzed} functions | avg cognitive: ${cx.avgCognitive} | avg cyclomatic: ${cx.avgCyclomatic} | max cognitive: ${cx.maxCognitive}${miPart}`,
211
- );
212
- }
213
-
214
- // Communities
215
- if (data.communities) {
216
- const cm = data.communities;
217
- console.log(
218
- `\nCommunities: ${cm.communityCount} detected | modularity: ${cm.modularity} | drift: ${cm.driftScore}%`,
219
- );
220
- }
221
-
222
- console.log();
223
- }
224
-
225
- // ─── queryName ──────────────────────────────────────────────────────────
226
-
227
- export function queryName(name, customDbPath, opts = {}) {
228
- const data = queryNameData(name, customDbPath, {
229
- noTests: opts.noTests,
230
- limit: opts.limit,
231
- offset: opts.offset,
232
- });
233
- if (outputResult(data, 'results', opts)) return;
234
-
235
- if (data.results.length === 0) {
236
- console.log(`No results for "${name}"`);
237
- return;
238
- }
239
-
240
- console.log(`\nResults for "${name}":\n`);
241
- for (const r of data.results) {
242
- console.log(` ${kindIcon(r.kind)} ${r.name} (${r.kind}) -- ${r.file}:${r.line}`);
243
- if (r.callees.length > 0) {
244
- console.log(` -> calls/uses:`);
245
- for (const c of r.callees.slice(0, 15))
246
- console.log(` -> ${c.name} (${c.edgeKind}) ${c.file}:${c.line}`);
247
- if (r.callees.length > 15) console.log(` ... and ${r.callees.length - 15} more`);
248
- }
249
- if (r.callers.length > 0) {
250
- console.log(` <- called by:`);
251
- for (const c of r.callers.slice(0, 15))
252
- console.log(` <- ${c.name} (${c.edgeKind}) ${c.file}:${c.line}`);
253
- if (r.callers.length > 15) console.log(` ... and ${r.callers.length - 15} more`);
254
- }
255
- console.log();
256
- }
257
- }
258
-
259
- // ─── impactAnalysis ─────────────────────────────────────────────────────
260
-
261
- export function impactAnalysis(file, customDbPath, opts = {}) {
262
- const data = impactAnalysisData(file, customDbPath, opts);
263
- if (outputResult(data, 'sources', opts)) return;
264
-
265
- if (data.sources.length === 0) {
266
- console.log(`No file matching "${file}" in graph`);
267
- return;
268
- }
269
-
270
- console.log(`\nImpact analysis for files matching "${file}":\n`);
271
- for (const s of data.sources) console.log(` # ${s} (source)`);
272
-
273
- const levels = data.levels;
274
- if (Object.keys(levels).length === 0) {
275
- console.log(` No dependents found.`);
276
- } else {
277
- for (const level of Object.keys(levels).sort((a, b) => a - b)) {
278
- const nodes = levels[level];
279
- console.log(
280
- `\n ${'--'.repeat(parseInt(level, 10))} Level ${level} (${nodes.length} files):`,
281
- );
282
- for (const n of nodes.slice(0, 30))
283
- console.log(` ${' '.repeat(parseInt(level, 10))}^ ${n.file}`);
284
- if (nodes.length > 30) console.log(` ... and ${nodes.length - 30} more`);
285
- }
286
- }
287
- console.log(`\n Total: ${data.totalDependents} files transitively depend on "${file}"\n`);
288
- }
289
-
290
- // ─── moduleMap ──────────────────────────────────────────────────────────
291
-
292
- export function moduleMap(customDbPath, limit = 20, opts = {}) {
293
- const data = moduleMapData(customDbPath, limit, { noTests: opts.noTests });
294
- if (outputResult(data, 'topNodes', opts)) return;
295
-
296
- console.log(`\nModule map (top ${limit} most-connected nodes):\n`);
297
- const dirs = new Map();
298
- for (const n of data.topNodes) {
299
- if (!dirs.has(n.dir)) dirs.set(n.dir, []);
300
- dirs.get(n.dir).push(n);
301
- }
302
- for (const [dir, files] of [...dirs].sort()) {
303
- console.log(` [${dir}/]`);
304
- for (const f of files) {
305
- const coupling = f.inEdges + f.outEdges;
306
- const bar = '#'.repeat(Math.min(coupling, 40));
307
- console.log(
308
- ` ${path.basename(f.file).padEnd(35)} <-${String(f.inEdges).padStart(3)} ->${String(f.outEdges).padStart(3)} =${String(coupling).padStart(3)} ${bar}`,
309
- );
310
- }
311
- }
312
- console.log(
313
- `\n Total: ${data.stats.totalFiles} files, ${data.stats.totalNodes} symbols, ${data.stats.totalEdges} edges\n`,
314
- );
315
- }
316
-
317
- // ─── fileDeps ───────────────────────────────────────────────────────────
318
-
319
- export function fileDeps(file, customDbPath, opts = {}) {
320
- const data = fileDepsData(file, customDbPath, opts);
321
- if (outputResult(data, 'results', opts)) return;
322
-
323
- if (data.results.length === 0) {
324
- console.log(`No file matching "${file}" in graph`);
325
- return;
326
- }
327
-
328
- for (const r of data.results) {
329
- console.log(`\n# ${r.file}\n`);
330
- console.log(` -> Imports (${r.imports.length}):`);
331
- for (const i of r.imports) {
332
- const typeTag = i.typeOnly ? ' (type-only)' : '';
333
- console.log(` -> ${i.file}${typeTag}`);
334
- }
335
- console.log(`\n <- Imported by (${r.importedBy.length}):`);
336
- for (const i of r.importedBy) console.log(` <- ${i.file}`);
337
- if (r.definitions.length > 0) {
338
- console.log(`\n Definitions (${r.definitions.length}):`);
339
- for (const d of r.definitions.slice(0, 30))
340
- console.log(` ${kindIcon(d.kind)} ${d.name} :${d.line}`);
341
- if (r.definitions.length > 30) console.log(` ... and ${r.definitions.length - 30} more`);
342
- }
343
- console.log();
344
- }
345
- }
346
-
347
- // ─── fnDeps ─────────────────────────────────────────────────────────────
348
-
349
- export function fnDeps(name, customDbPath, opts = {}) {
350
- const data = fnDepsData(name, customDbPath, opts);
351
- if (outputResult(data, 'results', opts)) return;
352
-
353
- if (data.results.length === 0) {
354
- console.log(`No function/method/class matching "${name}"`);
355
- return;
356
- }
357
-
358
- for (const r of data.results) {
359
- console.log(`\n${kindIcon(r.kind)} ${r.name} (${r.kind}) -- ${r.file}:${r.line}\n`);
360
- if (r.callees.length > 0) {
361
- console.log(` -> Calls (${r.callees.length}):`);
362
- for (const c of r.callees)
363
- console.log(` -> ${kindIcon(c.kind)} ${c.name} ${c.file}:${c.line}`);
364
- }
365
- if (r.callers.length > 0) {
366
- console.log(`\n <- Called by (${r.callers.length}):`);
367
- for (const c of r.callers) {
368
- const via = c.viaHierarchy ? ` (via ${c.viaHierarchy})` : '';
369
- console.log(` <- ${kindIcon(c.kind)} ${c.name} ${c.file}:${c.line}${via}`);
370
- }
371
- }
372
- for (const [d, fns] of Object.entries(r.transitiveCallers)) {
373
- console.log(
374
- `\n ${'<-'.repeat(parseInt(d, 10))} Transitive callers (depth ${d}, ${fns.length}):`,
375
- );
376
- for (const n of fns.slice(0, 20))
377
- console.log(
378
- ` ${' '.repeat(parseInt(d, 10) - 1)}<- ${kindIcon(n.kind)} ${n.name} ${n.file}:${n.line}`,
379
- );
380
- if (fns.length > 20) console.log(` ... and ${fns.length - 20} more`);
381
- }
382
- if (r.callees.length === 0 && r.callers.length === 0) {
383
- console.log(` (no call edges found -- may be invoked dynamically or via re-exports)`);
384
- }
385
- console.log();
386
- }
387
- }
388
-
389
- // ─── context ────────────────────────────────────────────────────────────
390
-
391
- export function context(name, customDbPath, opts = {}) {
392
- const data = contextData(name, customDbPath, opts);
393
- if (outputResult(data, 'results', opts)) return;
394
-
395
- if (data.results.length === 0) {
396
- console.log(`No function/method/class matching "${name}"`);
397
- return;
398
- }
399
-
400
- for (const r of data.results) {
401
- const lineRange = r.endLine ? `${r.line}-${r.endLine}` : `${r.line}`;
402
- const roleTag = r.role ? ` [${r.role}]` : '';
403
- console.log(`\n# ${r.name} (${r.kind})${roleTag} — ${r.file}:${lineRange}\n`);
404
-
405
- // Signature
406
- if (r.signature) {
407
- console.log('## Type/Shape Info');
408
- if (r.signature.params != null) console.log(` Parameters: (${r.signature.params})`);
409
- if (r.signature.returnType) console.log(` Returns: ${r.signature.returnType}`);
410
- console.log();
411
- }
412
-
413
- // Children
414
- if (r.children && r.children.length > 0) {
415
- console.log(`## Children (${r.children.length})`);
416
- for (const c of r.children) {
417
- console.log(` ${kindIcon(c.kind)} ${c.name} :${c.line}`);
418
- }
419
- console.log();
420
- }
421
-
422
- // Complexity
423
- if (r.complexity) {
424
- const cx = r.complexity;
425
- const miPart = cx.maintainabilityIndex ? ` | MI: ${cx.maintainabilityIndex}` : '';
426
- console.log('## Complexity');
427
- console.log(
428
- ` Cognitive: ${cx.cognitive} | Cyclomatic: ${cx.cyclomatic} | Max Nesting: ${cx.maxNesting}${miPart}`,
429
- );
430
- console.log();
431
- }
432
-
433
- // Source
434
- if (r.source) {
435
- console.log('## Source');
436
- for (const line of r.source.split('\n')) {
437
- console.log(` ${line}`);
438
- }
439
- console.log();
440
- }
441
-
442
- // Callees
443
- if (r.callees.length > 0) {
444
- console.log(`## Direct Dependencies (${r.callees.length})`);
445
- for (const c of r.callees) {
446
- const summary = c.summary ? ` — ${c.summary}` : '';
447
- console.log(` ${kindIcon(c.kind)} ${c.name} ${c.file}:${c.line}${summary}`);
448
- if (c.source) {
449
- for (const line of c.source.split('\n').slice(0, 10)) {
450
- console.log(` | ${line}`);
451
- }
452
- }
453
- }
454
- console.log();
455
- }
456
-
457
- // Callers
458
- if (r.callers.length > 0) {
459
- console.log(`## Callers (${r.callers.length})`);
460
- for (const c of r.callers) {
461
- const via = c.viaHierarchy ? ` (via ${c.viaHierarchy})` : '';
462
- console.log(` ${kindIcon(c.kind)} ${c.name} ${c.file}:${c.line}${via}`);
463
- }
464
- console.log();
465
- }
466
-
467
- // Related tests
468
- if (r.relatedTests.length > 0) {
469
- console.log('## Related Tests');
470
- for (const t of r.relatedTests) {
471
- console.log(` ${t.file} — ${t.testCount} tests`);
472
- for (const tn of t.testNames) {
473
- console.log(` - ${tn}`);
474
- }
475
- if (t.source) {
476
- console.log(' Source:');
477
- for (const line of t.source.split('\n').slice(0, 20)) {
478
- console.log(` | ${line}`);
479
- }
480
- }
481
- }
482
- console.log();
483
- }
484
-
485
- if (r.callees.length === 0 && r.callers.length === 0 && r.relatedTests.length === 0) {
486
- console.log(
487
- ' (no call edges or tests found — may be invoked dynamically or via re-exports)',
488
- );
489
- console.log();
490
- }
491
- }
492
- }
493
-
494
- // ─── children ───────────────────────────────────────────────────────────
495
-
496
- export function children(name, customDbPath, opts = {}) {
497
- const data = childrenData(name, customDbPath, opts);
498
- if (outputResult(data, 'results', opts)) return;
499
-
500
- if (data.results.length === 0) {
501
- console.log(`No symbol matching "${name}"`);
502
- return;
503
- }
504
- for (const r of data.results) {
505
- console.log(`\n${kindIcon(r.kind)} ${r.name} ${r.file}:${r.line}`);
506
- if (r.children.length === 0) {
507
- console.log(' (no children)');
508
- } else {
509
- for (const c of r.children) {
510
- console.log(` ${kindIcon(c.kind)} ${c.name} :${c.line}`);
511
- }
512
- }
513
- }
514
- }
515
-
516
- // ─── explain ────────────────────────────────────────────────────────────
517
-
518
- export function explain(target, customDbPath, opts = {}) {
519
- const data = explainData(target, customDbPath, opts);
520
- if (outputResult(data, 'results', opts)) return;
521
-
522
- if (data.results.length === 0) {
523
- console.log(`No ${data.kind === 'file' ? 'file' : 'function/symbol'} matching "${target}"`);
524
- return;
525
- }
526
-
527
- if (data.kind === 'file') {
528
- for (const r of data.results) {
529
- const publicCount = r.publicApi.length;
530
- const internalCount = r.internal.length;
531
- const lineInfo = r.lineCount ? `${r.lineCount} lines, ` : '';
532
- console.log(`\n# ${r.file}`);
533
- console.log(
534
- ` ${lineInfo}${r.symbolCount} symbols (${publicCount} exported, ${internalCount} internal)`,
535
- );
536
-
537
- if (r.imports.length > 0) {
538
- console.log(` Imports: ${r.imports.map((i) => i.file).join(', ')}`);
539
- }
540
- if (r.importedBy.length > 0) {
541
- console.log(` Imported by: ${r.importedBy.map((i) => i.file).join(', ')}`);
542
- }
543
-
544
- if (r.publicApi.length > 0) {
545
- console.log(`\n## Exported`);
546
- for (const s of r.publicApi) {
547
- const sig = s.signature?.params != null ? `(${s.signature.params})` : '';
548
- const roleTag = s.role ? ` [${s.role}]` : '';
549
- const summary = s.summary ? ` -- ${s.summary}` : '';
550
- console.log(` ${kindIcon(s.kind)} ${s.name}${sig}${roleTag} :${s.line}${summary}`);
551
- }
552
- }
553
-
554
- if (r.internal.length > 0) {
555
- console.log(`\n## Internal`);
556
- for (const s of r.internal) {
557
- const sig = s.signature?.params != null ? `(${s.signature.params})` : '';
558
- const roleTag = s.role ? ` [${s.role}]` : '';
559
- const summary = s.summary ? ` -- ${s.summary}` : '';
560
- console.log(` ${kindIcon(s.kind)} ${s.name}${sig}${roleTag} :${s.line}${summary}`);
561
- }
562
- }
563
-
564
- if (r.dataFlow.length > 0) {
565
- console.log(`\n## Data Flow`);
566
- for (const df of r.dataFlow) {
567
- console.log(` ${df.caller} -> ${df.callees.join(', ')}`);
568
- }
569
- }
570
- console.log();
571
- }
572
- } else {
573
- function printFunctionExplain(r, indent = '') {
574
- const lineRange = r.endLine ? `${r.line}-${r.endLine}` : `${r.line}`;
575
- const lineInfo = r.lineCount ? `${r.lineCount} lines` : '';
576
- const summaryPart = r.summary ? ` | ${r.summary}` : '';
577
- const roleTag = r.role ? ` [${r.role}]` : '';
578
- const depthLevel = r._depth || 0;
579
- const heading = depthLevel === 0 ? '#' : '##'.padEnd(depthLevel + 2, '#');
580
- console.log(`\n${indent}${heading} ${r.name} (${r.kind})${roleTag} ${r.file}:${lineRange}`);
581
- if (lineInfo || r.summary) {
582
- console.log(`${indent} ${lineInfo}${summaryPart}`);
583
- }
584
- if (r.signature) {
585
- if (r.signature.params != null)
586
- console.log(`${indent} Parameters: (${r.signature.params})`);
587
- if (r.signature.returnType) console.log(`${indent} Returns: ${r.signature.returnType}`);
588
- }
589
-
590
- if (r.complexity) {
591
- const cx = r.complexity;
592
- const miPart = cx.maintainabilityIndex ? ` MI=${cx.maintainabilityIndex}` : '';
593
- console.log(
594
- `${indent} Complexity: cognitive=${cx.cognitive} cyclomatic=${cx.cyclomatic} nesting=${cx.maxNesting}${miPart}`,
595
- );
596
- }
597
-
598
- if (r.callees.length > 0) {
599
- console.log(`\n${indent} Calls (${r.callees.length}):`);
600
- for (const c of r.callees) {
601
- console.log(`${indent} ${kindIcon(c.kind)} ${c.name} ${c.file}:${c.line}`);
602
- }
603
- }
604
-
605
- if (r.callers.length > 0) {
606
- console.log(`\n${indent} Called by (${r.callers.length}):`);
607
- for (const c of r.callers) {
608
- console.log(`${indent} ${kindIcon(c.kind)} ${c.name} ${c.file}:${c.line}`);
609
- }
610
- }
611
-
612
- if (r.relatedTests.length > 0) {
613
- const label = r.relatedTests.length === 1 ? 'file' : 'files';
614
- console.log(`\n${indent} Tests (${r.relatedTests.length} ${label}):`);
615
- for (const t of r.relatedTests) {
616
- console.log(`${indent} ${t.file}`);
617
- }
618
- }
619
-
620
- if (r.callees.length === 0 && r.callers.length === 0) {
621
- console.log(
622
- `${indent} (no call edges found -- may be invoked dynamically or via re-exports)`,
623
- );
624
- }
625
-
626
- // Render recursive dependency details
627
- if (r.depDetails && r.depDetails.length > 0) {
628
- console.log(`\n${indent} --- Dependencies (depth ${depthLevel + 1}) ---`);
629
- for (const dep of r.depDetails) {
630
- printFunctionExplain(dep, `${indent} `);
631
- }
632
- }
633
- console.log();
634
- }
635
-
636
- for (const r of data.results) {
637
- printFunctionExplain(r);
638
- }
639
- }
640
- }
641
-
642
- // ─── where ──────────────────────────────────────────────────────────────
643
-
644
- export function where(target, customDbPath, opts = {}) {
645
- const data = whereData(target, customDbPath, opts);
646
- if (outputResult(data, 'results', opts)) return;
647
-
648
- if (data.results.length === 0) {
649
- console.log(
650
- data.mode === 'file'
651
- ? `No file matching "${target}" in graph`
652
- : `No symbol matching "${target}" in graph`,
653
- );
654
- return;
655
- }
656
-
657
- if (data.mode === 'symbol') {
658
- for (const r of data.results) {
659
- const roleTag = r.role ? ` [${r.role}]` : '';
660
- const tag = r.exported ? ' (exported)' : '';
661
- console.log(`\n${kindIcon(r.kind)} ${r.name}${roleTag} ${r.file}:${r.line}${tag}`);
662
- if (r.uses.length > 0) {
663
- const useStrs = r.uses.map((u) => `${u.file}:${u.line}`);
664
- console.log(` Used in: ${useStrs.join(', ')}`);
665
- } else {
666
- console.log(' No uses found');
667
- }
668
- }
669
- } else {
670
- for (const r of data.results) {
671
- console.log(`\n# ${r.file}`);
672
- if (r.symbols.length > 0) {
673
- const symStrs = r.symbols.map((s) => `${s.name}:${s.line}`);
674
- console.log(` Symbols: ${symStrs.join(', ')}`);
675
- }
676
- if (r.imports.length > 0) {
677
- console.log(` Imports: ${r.imports.join(', ')}`);
678
- }
679
- if (r.importedBy.length > 0) {
680
- console.log(` Imported by: ${r.importedBy.join(', ')}`);
681
- }
682
- if (r.exported.length > 0) {
683
- console.log(` Exported: ${r.exported.join(', ')}`);
684
- }
685
- }
686
- }
687
- console.log();
688
- }
689
-
690
- // ─── roles ──────────────────────────────────────────────────────────────
691
-
692
- export function roles(customDbPath, opts = {}) {
693
- const data = rolesData(customDbPath, opts);
694
- if (outputResult(data, 'symbols', opts)) return;
695
-
696
- if (data.count === 0) {
697
- console.log('No classified symbols found. Run "codegraph build" first.');
698
- return;
699
- }
700
-
701
- const total = data.count;
702
- console.log(`\nNode roles (${total} symbols):\n`);
703
-
704
- const summaryParts = Object.entries(data.summary)
705
- .sort((a, b) => b[1] - a[1])
706
- .map(([role, count]) => `${role}: ${count}`);
707
- console.log(` ${summaryParts.join(' ')}\n`);
708
-
709
- const byRole = {};
710
- for (const s of data.symbols) {
711
- if (!byRole[s.role]) byRole[s.role] = [];
712
- byRole[s.role].push(s);
713
- }
714
-
715
- for (const [role, symbols] of Object.entries(byRole)) {
716
- console.log(`## ${role} (${symbols.length})`);
717
- for (const s of symbols.slice(0, 30)) {
718
- console.log(` ${kindIcon(s.kind)} ${s.name} ${s.file}:${s.line}`);
719
- }
720
- if (symbols.length > 30) {
721
- console.log(` ... and ${symbols.length - 30} more`);
722
- }
723
- console.log();
724
- }
725
- }
726
-
727
- // ─── fileExports ────────────────────────────────────────────────────────
728
-
729
- export function fileExports(file, customDbPath, opts = {}) {
730
- const data = exportsData(file, customDbPath, opts);
731
- if (outputResult(data, 'results', opts)) return;
732
-
733
- if (data.results.length === 0) {
734
- if (opts.unused) {
735
- console.log(`No unused exports found for "${file}".`);
736
- } else {
737
- console.log(`No exported symbols found for "${file}". Run "codegraph build" first.`);
738
- }
739
- return;
740
- }
741
-
742
- if (opts.unused) {
743
- console.log(
744
- `\n# ${data.file} — ${data.totalUnused} unused export${data.totalUnused !== 1 ? 's' : ''} (of ${data.totalExported} exported)\n`,
745
- );
746
- } else {
747
- const unusedNote = data.totalUnused > 0 ? ` (${data.totalUnused} unused)` : '';
748
- console.log(
749
- `\n# ${data.file} — ${data.totalExported} exported${unusedNote}, ${data.totalInternal} internal\n`,
750
- );
751
- }
752
-
753
- for (const sym of data.results) {
754
- const icon = kindIcon(sym.kind);
755
- const sig = sym.signature?.params ? `(${sym.signature.params})` : '';
756
- const role = sym.role ? ` [${sym.role}]` : '';
757
- console.log(` ${icon} ${sym.name}${sig}${role} :${sym.line}`);
758
- if (sym.consumers.length === 0) {
759
- console.log(' (no consumers)');
760
- } else {
761
- for (const c of sym.consumers) {
762
- console.log(` <- ${c.name} (${c.file}:${c.line})`);
763
- }
764
- }
765
- }
766
-
767
- if (data.reexports.length > 0) {
768
- console.log(`\n Re-exports: ${data.reexports.map((r) => r.file).join(', ')}`);
769
- }
770
- console.log();
771
- }
772
-
773
- // ─── fnImpact ───────────────────────────────────────────────────────────
774
-
775
- export function fnImpact(name, customDbPath, opts = {}) {
776
- const data = fnImpactData(name, customDbPath, opts);
777
- if (outputResult(data, 'results', opts)) return;
778
-
779
- if (data.results.length === 0) {
780
- console.log(`No function/method/class matching "${name}"`);
781
- return;
782
- }
783
-
784
- for (const r of data.results) {
785
- console.log(`\nFunction impact: ${kindIcon(r.kind)} ${r.name} -- ${r.file}:${r.line}\n`);
786
- if (Object.keys(r.levels).length === 0) {
787
- console.log(` No callers found.`);
788
- } else {
789
- for (const [level, fns] of Object.entries(r.levels).sort((a, b) => a[0] - b[0])) {
790
- const l = parseInt(level, 10);
791
- console.log(` ${'--'.repeat(l)} Level ${level} (${fns.length} functions):`);
792
- for (const f of fns.slice(0, 20))
793
- console.log(` ${' '.repeat(l)}^ ${kindIcon(f.kind)} ${f.name} ${f.file}:${f.line}`);
794
- if (fns.length > 20) console.log(` ... and ${fns.length - 20} more`);
795
- }
796
- }
797
- console.log(`\n Total: ${r.totalDependents} functions transitively depend on ${r.name}\n`);
798
- }
799
- }
800
-
801
- // ─── diffImpact ─────────────────────────────────────────────────────────
802
-
803
- export function diffImpact(customDbPath, opts = {}) {
804
- if (opts.format === 'mermaid') {
805
- console.log(diffImpactMermaid(customDbPath, opts));
806
- return;
807
- }
808
- const data = diffImpactData(customDbPath, opts);
809
- if (opts.format === 'json') opts = { ...opts, json: true };
810
- if (outputResult(data, 'affectedFunctions', opts)) return;
811
-
812
- if (data.error) {
813
- console.log(data.error);
814
- return;
815
- }
816
- if (data.changedFiles === 0) {
817
- console.log('No changes detected.');
818
- return;
819
- }
820
- if (data.affectedFunctions.length === 0) {
821
- console.log(
822
- ' No function-level changes detected (changes may be in imports, types, or config).',
823
- );
824
- return;
825
- }
826
-
827
- console.log(`\ndiff-impact: ${data.changedFiles} files changed\n`);
828
- console.log(` ${data.affectedFunctions.length} functions changed:\n`);
829
- for (const fn of data.affectedFunctions) {
830
- console.log(` ${kindIcon(fn.kind)} ${fn.name} -- ${fn.file}:${fn.line}`);
831
- if (fn.transitiveCallers > 0) console.log(` ^ ${fn.transitiveCallers} transitive callers`);
832
- }
833
- if (data.historicallyCoupled && data.historicallyCoupled.length > 0) {
834
- console.log('\n Historically coupled (not in static graph):\n');
835
- for (const c of data.historicallyCoupled) {
836
- const pct = `${(c.jaccard * 100).toFixed(0)}%`;
837
- console.log(
838
- ` ${c.file} <- coupled with ${c.coupledWith} (${pct}, ${c.commitCount} commits)`,
839
- );
840
- }
841
- }
842
- if (data.ownership) {
843
- console.log(`\n Affected owners: ${data.ownership.affectedOwners.join(', ')}`);
844
- console.log(` Suggested reviewers: ${data.ownership.suggestedReviewers.join(', ')}`);
845
- }
846
- if (data.boundaryViolations && data.boundaryViolations.length > 0) {
847
- console.log(`\n Boundary violations (${data.boundaryViolationCount}):\n`);
848
- for (const v of data.boundaryViolations) {
849
- console.log(` [${v.name}] ${v.file} -> ${v.targetFile}`);
850
- if (v.message) console.log(` ${v.message}`);
851
- }
852
- }
853
- if (data.summary) {
854
- let summaryLine = `\n Summary: ${data.summary.functionsChanged} functions changed -> ${data.summary.callersAffected} callers affected across ${data.summary.filesAffected} files`;
855
- if (data.summary.historicallyCoupledCount > 0) {
856
- summaryLine += `, ${data.summary.historicallyCoupledCount} historically coupled`;
857
- }
858
- if (data.summary.ownersAffected > 0) {
859
- summaryLine += `, ${data.summary.ownersAffected} owners affected`;
860
- }
861
- if (data.summary.boundaryViolationCount > 0) {
862
- summaryLine += `, ${data.summary.boundaryViolationCount} boundary violations`;
863
- }
864
- console.log(`${summaryLine}\n`);
865
- }
866
- }