@optave/codegraph 3.1.1 → 3.1.3

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 (72) hide show
  1. package/README.md +6 -6
  2. package/package.json +7 -7
  3. package/src/ast-analysis/engine.js +365 -0
  4. package/src/ast-analysis/metrics.js +118 -0
  5. package/src/ast-analysis/visitor-utils.js +176 -0
  6. package/src/ast-analysis/visitor.js +162 -0
  7. package/src/ast-analysis/visitors/ast-store-visitor.js +150 -0
  8. package/src/ast-analysis/visitors/cfg-visitor.js +792 -0
  9. package/src/ast-analysis/visitors/complexity-visitor.js +243 -0
  10. package/src/ast-analysis/visitors/dataflow-visitor.js +358 -0
  11. package/src/ast.js +13 -140
  12. package/src/audit.js +2 -87
  13. package/src/batch.js +0 -25
  14. package/src/boundaries.js +1 -1
  15. package/src/branch-compare.js +1 -96
  16. package/src/builder.js +60 -178
  17. package/src/cfg.js +89 -883
  18. package/src/check.js +1 -84
  19. package/src/cli.js +31 -22
  20. package/src/cochange.js +1 -39
  21. package/src/commands/audit.js +88 -0
  22. package/src/commands/batch.js +26 -0
  23. package/src/commands/branch-compare.js +97 -0
  24. package/src/commands/cfg.js +55 -0
  25. package/src/commands/check.js +82 -0
  26. package/src/commands/cochange.js +37 -0
  27. package/src/commands/communities.js +69 -0
  28. package/src/commands/complexity.js +77 -0
  29. package/src/commands/dataflow.js +110 -0
  30. package/src/commands/flow.js +70 -0
  31. package/src/commands/manifesto.js +77 -0
  32. package/src/commands/owners.js +52 -0
  33. package/src/commands/query.js +21 -0
  34. package/src/commands/sequence.js +33 -0
  35. package/src/commands/structure.js +64 -0
  36. package/src/commands/triage.js +49 -0
  37. package/src/communities.js +12 -83
  38. package/src/complexity.js +43 -357
  39. package/src/cycles.js +1 -1
  40. package/src/dataflow.js +12 -665
  41. package/src/db/repository/build-stmts.js +104 -0
  42. package/src/db/repository/cached-stmt.js +19 -0
  43. package/src/db/repository/cfg.js +72 -0
  44. package/src/db/repository/cochange.js +54 -0
  45. package/src/db/repository/complexity.js +20 -0
  46. package/src/db/repository/dataflow.js +17 -0
  47. package/src/db/repository/edges.js +281 -0
  48. package/src/db/repository/embeddings.js +51 -0
  49. package/src/db/repository/graph-read.js +59 -0
  50. package/src/db/repository/index.js +43 -0
  51. package/src/db/repository/nodes.js +247 -0
  52. package/src/db.js +40 -1
  53. package/src/embedder.js +14 -34
  54. package/src/export.js +1 -1
  55. package/src/extractors/javascript.js +130 -5
  56. package/src/flow.js +2 -70
  57. package/src/index.js +30 -20
  58. package/src/{result-formatter.js → infrastructure/result-formatter.js} +1 -1
  59. package/src/kinds.js +1 -0
  60. package/src/manifesto.js +0 -76
  61. package/src/native.js +31 -9
  62. package/src/owners.js +1 -56
  63. package/src/parser.js +53 -2
  64. package/src/queries-cli.js +1 -1
  65. package/src/queries.js +79 -280
  66. package/src/sequence.js +5 -44
  67. package/src/structure.js +16 -75
  68. package/src/triage.js +1 -54
  69. package/src/viewer.js +1 -1
  70. package/src/watcher.js +7 -4
  71. package/src/db/repository.js +0 -134
  72. /package/src/{test-filter.js → infrastructure/test-filter.js} +0 -0
package/src/index.js CHANGED
@@ -8,13 +8,11 @@
8
8
  // AST node queries
9
9
  export { AST_NODE_KINDS, astQuery, astQueryData } from './ast.js';
10
10
  // Audit (composite report)
11
- export { audit, auditData } from './audit.js';
11
+ export { auditData } from './audit.js';
12
12
  // Batch querying
13
13
  export {
14
14
  BATCH_COMMANDS,
15
- batch,
16
15
  batchData,
17
- batchQuery,
18
16
  multiBatchData,
19
17
  splitTargets,
20
18
  } from './batch.js';
@@ -29,13 +27,12 @@ export {
29
27
  buildCFGData,
30
28
  buildFunctionCFG,
31
29
  CFG_RULES,
32
- cfg,
33
30
  cfgData,
34
31
  cfgToDOT,
35
32
  cfgToMermaid,
36
33
  } from './cfg.js';
37
34
  // Check (CI validation predicates)
38
- export { check, checkData } from './check.js';
35
+ export { checkData } from './check.js';
39
36
  // Co-change analysis
40
37
  export {
41
38
  analyzeCoChanges,
@@ -45,12 +42,23 @@ export {
45
42
  computeCoChanges,
46
43
  scanGitHistory,
47
44
  } from './cochange.js';
45
+ export { audit } from './commands/audit.js';
46
+ export { batch, batchQuery } from './commands/batch.js';
47
+ export { cfg } from './commands/cfg.js';
48
+ export { check } from './commands/check.js';
49
+ export { communities } from './commands/communities.js';
50
+ export { complexity } from './commands/complexity.js';
51
+ export { dataflow } from './commands/dataflow.js';
52
+ export { manifesto } from './commands/manifesto.js';
53
+ export { owners } from './commands/owners.js';
54
+ export { sequence } from './commands/sequence.js';
55
+ export { formatHotspots, formatModuleBoundaries, formatStructure } from './commands/structure.js';
56
+ export { triage } from './commands/triage.js';
48
57
  // Community detection
49
- export { communities, communitiesData, communitySummaryForStats } from './communities.js';
58
+ export { communitiesData, communitySummaryForStats } from './communities.js';
50
59
  // Complexity metrics
51
60
  export {
52
61
  COMPLEXITY_RULES,
53
- complexity,
54
62
  complexityData,
55
63
  computeFunctionComplexity,
56
64
  computeHalsteadMetrics,
@@ -69,7 +77,6 @@ export { findCycles, formatCycles } from './cycles.js';
69
77
  // Dataflow analysis
70
78
  export {
71
79
  buildDataflowEdges,
72
- dataflow,
73
80
  dataflowData,
74
81
  dataflowImpactData,
75
82
  dataflowPathData,
@@ -123,18 +130,28 @@ export {
123
130
  } from './export.js';
124
131
  // Execution flow tracing
125
132
  export { entryPointType, flowData, listEntryPointsData } from './flow.js';
133
+ // Result formatting
134
+ export { outputResult } from './infrastructure/result-formatter.js';
135
+ // Test file detection
136
+ export { isTestFile, TEST_PATTERN } from './infrastructure/test-filter.js';
126
137
  // Logger
127
138
  export { setVerbose } from './logger.js';
128
139
  // Manifesto rule engine
129
- export { manifesto, manifestoData, RULE_DEFS } from './manifesto.js';
140
+ export { manifestoData, RULE_DEFS } from './manifesto.js';
130
141
  // Native engine
131
142
  export { isNativeAvailable } from './native.js';
132
143
  // Ownership (CODEOWNERS)
133
- export { matchOwners, owners, ownersData, ownersForFiles, parseCodeowners } from './owners.js';
144
+ export { matchOwners, ownersData, ownersForFiles, parseCodeowners } from './owners.js';
134
145
  // Pagination utilities
135
146
  export { MCP_DEFAULTS, MCP_MAX_LIMIT, paginate, paginateResult, printNdjson } from './paginate.js';
136
147
  // Unified parser API
137
- export { getActiveEngine, isWasmAvailable, parseFileAuto, parseFilesAuto } from './parser.js';
148
+ export {
149
+ disposeParsers,
150
+ getActiveEngine,
151
+ isWasmAvailable,
152
+ parseFileAuto,
153
+ parseFilesAuto,
154
+ } from './parser.js';
138
155
  // Query functions (data-returning)
139
156
  export {
140
157
  ALL_SYMBOL_KINDS,
@@ -198,10 +215,8 @@ export {
198
215
  saveRegistry,
199
216
  unregisterRepo,
200
217
  } from './registry.js';
201
- // Result formatting
202
- export { outputResult } from './result-formatter.js';
203
218
  // Sequence diagram generation
204
- export { sequence, sequenceData, sequenceToMermaid } from './sequence.js';
219
+ export { sequenceData, sequenceToMermaid } from './sequence.js';
205
220
  // Snapshot management
206
221
  export {
207
222
  snapshotDelete,
@@ -216,17 +231,12 @@ export {
216
231
  buildStructure,
217
232
  classifyNodeRoles,
218
233
  FRAMEWORK_ENTRY_PREFIXES,
219
- formatHotspots,
220
- formatModuleBoundaries,
221
- formatStructure,
222
234
  hotspotsData,
223
235
  moduleBoundariesData,
224
236
  structureData,
225
237
  } from './structure.js';
226
- // Test file detection
227
- export { isTestFile, TEST_PATTERN } from './test-filter.js';
228
238
  // Triage — composite risk audit
229
- export { triage, triageData } from './triage.js';
239
+ export { triageData } from './triage.js';
230
240
  // Interactive HTML viewer
231
241
  export { generatePlotHTML, loadPlotConfig } from './viewer.js';
232
242
  // Watch mode
@@ -1,4 +1,4 @@
1
- import { printNdjson } from './paginate.js';
1
+ import { printNdjson } from '../paginate.js';
2
2
 
3
3
  /**
4
4
  * Shared JSON / NDJSON output dispatch for CLI wrappers.
package/src/kinds.js CHANGED
@@ -33,6 +33,7 @@ export const ALL_SYMBOL_KINDS = CORE_SYMBOL_KINDS;
33
33
  export const CORE_EDGE_KINDS = [
34
34
  'imports',
35
35
  'imports-type',
36
+ 'dynamic-imports',
36
37
  'reexports',
37
38
  'calls',
38
39
  'extends',
package/src/manifesto.js CHANGED
@@ -4,7 +4,6 @@ import { findCycles } from './cycles.js';
4
4
  import { openReadonlyOrFail } from './db.js';
5
5
  import { debug } from './logger.js';
6
6
  import { paginateResult } from './paginate.js';
7
- import { outputResult } from './result-formatter.js';
8
7
 
9
8
  // ─── Rule Definitions ─────────────────────────────────────────────────
10
9
 
@@ -428,78 +427,3 @@ export function manifestoData(customDbPath, opts = {}) {
428
427
  db.close();
429
428
  }
430
429
  }
431
-
432
- /**
433
- * CLI formatter — prints manifesto results and exits with code 1 on failure.
434
- */
435
- export function manifesto(customDbPath, opts = {}) {
436
- const data = manifestoData(customDbPath, opts);
437
-
438
- if (outputResult(data, 'violations', opts)) {
439
- if (!data.passed) process.exit(1);
440
- return;
441
- }
442
-
443
- console.log('\n# Manifesto Rules\n');
444
-
445
- // Rules table
446
- console.log(
447
- ` ${'Rule'.padEnd(20)} ${'Level'.padEnd(10)} ${'Status'.padEnd(8)} ${'Warn'.padStart(6)} ${'Fail'.padStart(6)} ${'Violations'.padStart(11)}`,
448
- );
449
- console.log(
450
- ` ${'─'.repeat(20)} ${'─'.repeat(10)} ${'─'.repeat(8)} ${'─'.repeat(6)} ${'─'.repeat(6)} ${'─'.repeat(11)}`,
451
- );
452
-
453
- for (const rule of data.rules) {
454
- const warn = rule.thresholds.warn != null ? String(rule.thresholds.warn) : '—';
455
- const fail = rule.thresholds.fail != null ? String(rule.thresholds.fail) : '—';
456
- const statusIcon = rule.status === 'pass' ? 'pass' : rule.status === 'warn' ? 'WARN' : 'FAIL';
457
- console.log(
458
- ` ${rule.name.padEnd(20)} ${rule.level.padEnd(10)} ${statusIcon.padEnd(8)} ${warn.padStart(6)} ${fail.padStart(6)} ${String(rule.violationCount).padStart(11)}`,
459
- );
460
- }
461
-
462
- // Summary
463
- const s = data.summary;
464
- console.log(
465
- `\n ${s.total} rules | ${s.passed} passed | ${s.warned} warned | ${s.failed} failed | ${s.violationCount} violations`,
466
- );
467
-
468
- // Violations detail
469
- if (data.violations.length > 0) {
470
- const failViolations = data.violations.filter((v) => v.level === 'fail');
471
- const warnViolations = data.violations.filter((v) => v.level === 'warn');
472
-
473
- if (failViolations.length > 0) {
474
- console.log(`\n## Failures (${failViolations.length})\n`);
475
- for (const v of failViolations.slice(0, 20)) {
476
- const loc = v.line ? `${v.file}:${v.line}` : v.file;
477
- console.log(
478
- ` [FAIL] ${v.rule}: ${v.name} (${v.value}) at ${loc} — threshold ${v.threshold}`,
479
- );
480
- }
481
- if (failViolations.length > 20) {
482
- console.log(` ... and ${failViolations.length - 20} more`);
483
- }
484
- }
485
-
486
- if (warnViolations.length > 0) {
487
- console.log(`\n## Warnings (${warnViolations.length})\n`);
488
- for (const v of warnViolations.slice(0, 20)) {
489
- const loc = v.line ? `${v.file}:${v.line}` : v.file;
490
- console.log(
491
- ` [WARN] ${v.rule}: ${v.name} (${v.value}) at ${loc} — threshold ${v.threshold}`,
492
- );
493
- }
494
- if (warnViolations.length > 20) {
495
- console.log(` ... and ${warnViolations.length - 20} more`);
496
- }
497
- }
498
- }
499
-
500
- console.log();
501
-
502
- if (!data.passed) {
503
- process.exit(1);
504
- }
505
- }
package/src/native.js CHANGED
@@ -11,6 +11,7 @@ import os from 'node:os';
11
11
 
12
12
  let _cached; // undefined = not yet tried, null = failed, object = module
13
13
  let _loadError = null;
14
+ const _require = createRequire(import.meta.url);
14
15
 
15
16
  /**
16
17
  * Detect whether the current Linux environment uses glibc or musl.
@@ -18,7 +19,7 @@ let _loadError = null;
18
19
  */
19
20
  function detectLibc() {
20
21
  try {
21
- const { readdirSync } = require('node:fs');
22
+ const { readdirSync } = _require('node:fs');
22
23
  const files = readdirSync('/lib');
23
24
  if (files.some((f) => f.startsWith('ld-musl-') && f.endsWith('.so.1'))) {
24
25
  return 'musl';
@@ -38,6 +39,17 @@ const PLATFORM_PACKAGES = {
38
39
  'win32-x64': '@optave/codegraph-win32-x64-msvc',
39
40
  };
40
41
 
42
+ /**
43
+ * Resolve the platform-specific npm package name for the native addon.
44
+ * Returns null if the current platform is not supported.
45
+ */
46
+ function resolvePlatformPackage() {
47
+ const platform = os.platform();
48
+ const arch = os.arch();
49
+ const key = platform === 'linux' ? `${platform}-${arch}-${detectLibc()}` : `${platform}-${arch}`;
50
+ return PLATFORM_PACKAGES[key] || null;
51
+ }
52
+
41
53
  /**
42
54
  * Try to load the native napi addon.
43
55
  * Returns the module on success, null on failure.
@@ -45,21 +57,16 @@ const PLATFORM_PACKAGES = {
45
57
  export function loadNative() {
46
58
  if (_cached !== undefined) return _cached;
47
59
 
48
- const require = createRequire(import.meta.url);
49
-
50
- const platform = os.platform();
51
- const arch = os.arch();
52
- const key = platform === 'linux' ? `${platform}-${arch}-${detectLibc()}` : `${platform}-${arch}`;
53
- const pkg = PLATFORM_PACKAGES[key];
60
+ const pkg = resolvePlatformPackage();
54
61
  if (pkg) {
55
62
  try {
56
- _cached = require(pkg);
63
+ _cached = _require(pkg);
57
64
  return _cached;
58
65
  } catch (err) {
59
66
  _loadError = err;
60
67
  }
61
68
  } else {
62
- _loadError = new Error(`Unsupported platform: ${key}`);
69
+ _loadError = new Error(`Unsupported platform: ${os.platform()}-${os.arch()}`);
63
70
  }
64
71
 
65
72
  _cached = null;
@@ -73,6 +80,21 @@ export function isNativeAvailable() {
73
80
  return loadNative() !== null;
74
81
  }
75
82
 
83
+ /**
84
+ * Read the version from the platform-specific npm package.json.
85
+ * Returns null if the package is not installed or has no version.
86
+ */
87
+ export function getNativePackageVersion() {
88
+ const pkg = resolvePlatformPackage();
89
+ if (!pkg) return null;
90
+ try {
91
+ const pkgJson = _require(`${pkg}/package.json`);
92
+ return pkgJson.version || null;
93
+ } catch {
94
+ return null;
95
+ }
96
+ }
97
+
76
98
  /**
77
99
  * Return the native module or throw if not available.
78
100
  */
package/src/owners.js CHANGED
@@ -1,8 +1,7 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import { findDbPath, openReadonlyOrFail } from './db.js';
4
- import { outputResult } from './result-formatter.js';
5
- import { isTestFile } from './test-filter.js';
4
+ import { isTestFile } from './infrastructure/test-filter.js';
6
5
 
7
6
  // ─── CODEOWNERS Parsing ──────────────────────────────────────────────
8
7
 
@@ -303,57 +302,3 @@ export function ownersData(customDbPath, opts = {}) {
303
302
  db.close();
304
303
  }
305
304
  }
306
-
307
- // ─── CLI Display ─────────────────────────────────────────────────────
308
-
309
- /**
310
- * CLI display function for the `owners` command.
311
- * @param {string} [customDbPath]
312
- * @param {object} [opts]
313
- */
314
- export function owners(customDbPath, opts = {}) {
315
- const data = ownersData(customDbPath, opts);
316
- if (outputResult(data, null, opts)) return;
317
-
318
- if (!data.codeownersFile) {
319
- console.log('No CODEOWNERS file found.');
320
- return;
321
- }
322
-
323
- console.log(`\nCODEOWNERS: ${data.codeownersFile}\n`);
324
-
325
- const s = data.summary;
326
- console.log(
327
- ` Coverage: ${s.coveragePercent}% (${s.ownedFiles}/${s.totalFiles} files owned, ${s.ownerCount} owners)\n`,
328
- );
329
-
330
- if (s.byOwner.length > 0) {
331
- console.log(' Owners:\n');
332
- for (const o of s.byOwner) {
333
- console.log(` ${o.owner} ${o.fileCount} files`);
334
- }
335
- console.log();
336
- }
337
-
338
- if (data.files.length > 0 && opts.owner) {
339
- console.log(` Files owned by ${opts.owner}:\n`);
340
- for (const f of data.files) {
341
- console.log(` ${f.file}`);
342
- }
343
- console.log();
344
- }
345
-
346
- if (data.boundaries.length > 0) {
347
- console.log(` Cross-owner boundaries: ${data.boundaries.length} edges\n`);
348
- const shown = data.boundaries.slice(0, 30);
349
- for (const b of shown) {
350
- const srcOwner = b.from.owners.join(', ') || '(unowned)';
351
- const tgtOwner = b.to.owners.join(', ') || '(unowned)';
352
- console.log(` ${b.from.name} [${srcOwner}] -> ${b.to.name} [${tgtOwner}]`);
353
- }
354
- if (data.boundaries.length > 30) {
355
- console.log(` ... and ${data.boundaries.length - 30} more`);
356
- }
357
- console.log();
358
- }
359
- }
package/src/parser.js CHANGED
@@ -3,7 +3,7 @@ import path from 'node:path';
3
3
  import { fileURLToPath } from 'node:url';
4
4
  import { Language, Parser, Query } from 'web-tree-sitter';
5
5
  import { warn } from './logger.js';
6
- import { getNative, loadNative } from './native.js';
6
+ import { getNative, getNativePackageVersion, loadNative } from './native.js';
7
7
 
8
8
  // Re-export all extractors for backward compatibility
9
9
  export {
@@ -41,6 +41,9 @@ let _initialized = false;
41
41
  // Memoized parsers — avoids reloading WASM grammars on every createParsers() call
42
42
  let _cachedParsers = null;
43
43
 
44
+ // Cached Language objects — WASM-backed, must be .delete()'d explicitly
45
+ let _cachedLanguages = null;
46
+
44
47
  // Query cache for JS/TS/TSX extractors (populated during createParsers)
45
48
  const _queryCache = new Map();
46
49
 
@@ -77,12 +80,14 @@ export async function createParsers() {
77
80
  }
78
81
 
79
82
  const parsers = new Map();
83
+ const languages = new Map();
80
84
  for (const entry of LANGUAGE_REGISTRY) {
81
85
  try {
82
86
  const lang = await Language.load(grammarPath(entry.grammarFile));
83
87
  const parser = new Parser();
84
88
  parser.setLanguage(lang);
85
89
  parsers.set(entry.id, parser);
90
+ languages.set(entry.id, lang);
86
91
  // Compile and cache tree-sitter Query for JS/TS/TSX extractors
87
92
  if (entry.extractor === extractSymbols && !_queryCache.has(entry.id)) {
88
93
  const isTS = entry.id === 'typescript' || entry.id === 'tsx';
@@ -100,9 +105,47 @@ export async function createParsers() {
100
105
  }
101
106
  }
102
107
  _cachedParsers = parsers;
108
+ _cachedLanguages = languages;
103
109
  return parsers;
104
110
  }
105
111
 
112
+ /**
113
+ * Dispose all cached WASM parsers and queries to free WASM linear memory.
114
+ * Call this between repeated builds in the same process (e.g. benchmarks)
115
+ * to prevent memory accumulation that can cause segfaults.
116
+ */
117
+ export function disposeParsers() {
118
+ if (_cachedParsers) {
119
+ for (const [, parser] of _cachedParsers) {
120
+ if (parser && typeof parser.delete === 'function') {
121
+ try {
122
+ parser.delete();
123
+ } catch {}
124
+ }
125
+ }
126
+ _cachedParsers = null;
127
+ }
128
+ for (const [, query] of _queryCache) {
129
+ if (query && typeof query.delete === 'function') {
130
+ try {
131
+ query.delete();
132
+ } catch {}
133
+ }
134
+ }
135
+ _queryCache.clear();
136
+ if (_cachedLanguages) {
137
+ for (const [, lang] of _cachedLanguages) {
138
+ if (lang && typeof lang.delete === 'function') {
139
+ try {
140
+ lang.delete();
141
+ } catch {}
142
+ }
143
+ }
144
+ _cachedLanguages = null;
145
+ }
146
+ _initialized = false;
147
+ }
148
+
106
149
  export function getParser(parsers, filePath) {
107
150
  const ext = path.extname(filePath);
108
151
  const entry = _extToLang.get(ext);
@@ -214,6 +257,7 @@ function patchNativeResult(r) {
214
257
  if (i.csharpUsing === undefined) i.csharpUsing = i.csharp_using;
215
258
  if (i.rubyRequire === undefined) i.rubyRequire = i.ruby_require;
216
259
  if (i.phpUse === undefined) i.phpUse = i.php_use;
260
+ if (i.dynamicImport === undefined) i.dynamicImport = i.dynamic_import;
217
261
  }
218
262
  }
219
263
 
@@ -429,11 +473,18 @@ export async function parseFilesAuto(filePaths, rootDir, opts = {}) {
429
473
  */
430
474
  export function getActiveEngine(opts = {}) {
431
475
  const { name, native } = resolveEngine(opts);
432
- const version = native
476
+ let version = native
433
477
  ? typeof native.engineVersion === 'function'
434
478
  ? native.engineVersion()
435
479
  : null
436
480
  : null;
481
+ // Prefer platform package.json version over binary-embedded version
482
+ // to handle stale binaries that weren't recompiled during a release
483
+ if (native) {
484
+ try {
485
+ version = getNativePackageVersion() ?? version;
486
+ } catch {}
487
+ }
437
488
  return { name, version };
438
489
  }
439
490
 
@@ -7,6 +7,7 @@
7
7
  */
8
8
 
9
9
  import path from 'node:path';
10
+ import { outputResult } from './infrastructure/result-formatter.js';
10
11
  import {
11
12
  childrenData,
12
13
  contextData,
@@ -26,7 +27,6 @@ import {
26
27
  statsData,
27
28
  whereData,
28
29
  } from './queries.js';
29
- import { outputResult } from './result-formatter.js';
30
30
 
31
31
  // ─── symbolPath ─────────────────────────────────────────────────────────
32
32