@optave/codegraph 3.8.0 → 3.9.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 (310) hide show
  1. package/README.md +13 -8
  2. package/dist/ast-analysis/engine.d.ts.map +1 -1
  3. package/dist/ast-analysis/engine.js +137 -86
  4. package/dist/ast-analysis/engine.js.map +1 -1
  5. package/dist/ast-analysis/metrics.d.ts +0 -3
  6. package/dist/ast-analysis/metrics.d.ts.map +1 -1
  7. package/dist/ast-analysis/metrics.js +30 -13
  8. package/dist/ast-analysis/metrics.js.map +1 -1
  9. package/dist/ast-analysis/shared.d.ts.map +1 -1
  10. package/dist/ast-analysis/shared.js +24 -19
  11. package/dist/ast-analysis/shared.js.map +1 -1
  12. package/dist/ast-analysis/visitor-utils.d.ts.map +1 -1
  13. package/dist/ast-analysis/visitor-utils.js +55 -39
  14. package/dist/ast-analysis/visitor-utils.js.map +1 -1
  15. package/dist/ast-analysis/visitor.d.ts.map +1 -1
  16. package/dist/ast-analysis/visitor.js +91 -70
  17. package/dist/ast-analysis/visitor.js.map +1 -1
  18. package/dist/ast-analysis/visitors/ast-store-visitor.d.ts.map +1 -1
  19. package/dist/ast-analysis/visitors/ast-store-visitor.js +54 -58
  20. package/dist/ast-analysis/visitors/ast-store-visitor.js.map +1 -1
  21. package/dist/ast-analysis/visitors/complexity-visitor.d.ts.map +1 -1
  22. package/dist/ast-analysis/visitors/complexity-visitor.js +81 -39
  23. package/dist/ast-analysis/visitors/complexity-visitor.js.map +1 -1
  24. package/dist/ast-analysis/visitors/dataflow-visitor.d.ts.map +1 -1
  25. package/dist/ast-analysis/visitors/dataflow-visitor.js +57 -38
  26. package/dist/ast-analysis/visitors/dataflow-visitor.js.map +1 -1
  27. package/dist/cli/commands/branch-compare.d.ts.map +1 -1
  28. package/dist/cli/commands/branch-compare.js +4 -0
  29. package/dist/cli/commands/branch-compare.js.map +1 -1
  30. package/dist/cli/commands/diff-impact.d.ts.map +1 -1
  31. package/dist/cli/commands/diff-impact.js +2 -1
  32. package/dist/cli/commands/diff-impact.js.map +1 -1
  33. package/dist/cli/commands/info.d.ts.map +1 -1
  34. package/dist/cli/commands/info.js +3 -2
  35. package/dist/cli/commands/info.js.map +1 -1
  36. package/dist/cli/commands/watch.d.ts.map +1 -1
  37. package/dist/cli/commands/watch.js +16 -2
  38. package/dist/cli/commands/watch.js.map +1 -1
  39. package/dist/db/connection.d.ts.map +1 -1
  40. package/dist/db/connection.js +29 -26
  41. package/dist/db/connection.js.map +1 -1
  42. package/dist/db/query-builder.d.ts.map +1 -1
  43. package/dist/db/query-builder.js +16 -5
  44. package/dist/db/query-builder.js.map +1 -1
  45. package/dist/db/repository/base.d.ts +16 -0
  46. package/dist/db/repository/base.d.ts.map +1 -1
  47. package/dist/db/repository/base.js +31 -0
  48. package/dist/db/repository/base.js.map +1 -1
  49. package/dist/db/repository/native-repository.d.ts +7 -1
  50. package/dist/db/repository/native-repository.d.ts.map +1 -1
  51. package/dist/db/repository/native-repository.js +100 -1
  52. package/dist/db/repository/native-repository.js.map +1 -1
  53. package/dist/db/repository/nodes.d.ts.map +1 -1
  54. package/dist/db/repository/nodes.js +8 -4
  55. package/dist/db/repository/nodes.js.map +1 -1
  56. package/dist/db/repository/sqlite-repository.d.ts +4 -0
  57. package/dist/db/repository/sqlite-repository.d.ts.map +1 -1
  58. package/dist/db/repository/sqlite-repository.js +51 -0
  59. package/dist/db/repository/sqlite-repository.js.map +1 -1
  60. package/dist/domain/analysis/brief.d.ts.map +1 -1
  61. package/dist/domain/analysis/brief.js +13 -17
  62. package/dist/domain/analysis/brief.js.map +1 -1
  63. package/dist/domain/analysis/context.d.ts.map +1 -1
  64. package/dist/domain/analysis/context.js +14 -11
  65. package/dist/domain/analysis/context.js.map +1 -1
  66. package/dist/domain/analysis/dependencies.d.ts.map +1 -1
  67. package/dist/domain/analysis/dependencies.js +64 -59
  68. package/dist/domain/analysis/dependencies.js.map +1 -1
  69. package/dist/domain/analysis/fn-impact.d.ts +2 -7
  70. package/dist/domain/analysis/fn-impact.d.ts.map +1 -1
  71. package/dist/domain/analysis/fn-impact.js +33 -31
  72. package/dist/domain/analysis/fn-impact.js.map +1 -1
  73. package/dist/domain/analysis/implementations.d.ts.map +1 -1
  74. package/dist/domain/analysis/implementations.js +11 -19
  75. package/dist/domain/analysis/implementations.js.map +1 -1
  76. package/dist/domain/analysis/module-map.d.ts.map +1 -1
  77. package/dist/domain/analysis/module-map.js +55 -76
  78. package/dist/domain/analysis/module-map.js.map +1 -1
  79. package/dist/domain/analysis/query-helpers.d.ts +7 -0
  80. package/dist/domain/analysis/query-helpers.d.ts.map +1 -1
  81. package/dist/domain/analysis/query-helpers.js +15 -1
  82. package/dist/domain/analysis/query-helpers.js.map +1 -1
  83. package/dist/domain/graph/builder/pipeline.d.ts.map +1 -1
  84. package/dist/domain/graph/builder/pipeline.js +352 -107
  85. package/dist/domain/graph/builder/pipeline.js.map +1 -1
  86. package/dist/domain/graph/builder/stages/build-edges.d.ts.map +1 -1
  87. package/dist/domain/graph/builder/stages/build-edges.js +49 -18
  88. package/dist/domain/graph/builder/stages/build-edges.js.map +1 -1
  89. package/dist/domain/graph/builder/stages/detect-changes.js +2 -2
  90. package/dist/domain/graph/builder/stages/detect-changes.js.map +1 -1
  91. package/dist/domain/graph/builder/stages/finalize.js +2 -2
  92. package/dist/domain/graph/builder/stages/finalize.js.map +1 -1
  93. package/dist/domain/graph/builder/stages/insert-nodes.d.ts.map +1 -1
  94. package/dist/domain/graph/builder/stages/insert-nodes.js +32 -21
  95. package/dist/domain/graph/builder/stages/insert-nodes.js.map +1 -1
  96. package/dist/domain/graph/builder/stages/resolve-imports.d.ts.map +1 -1
  97. package/dist/domain/graph/builder/stages/resolve-imports.js +95 -84
  98. package/dist/domain/graph/builder/stages/resolve-imports.js.map +1 -1
  99. package/dist/domain/graph/cycles.d.ts +6 -0
  100. package/dist/domain/graph/cycles.d.ts.map +1 -1
  101. package/dist/domain/graph/cycles.js +114 -22
  102. package/dist/domain/graph/cycles.js.map +1 -1
  103. package/dist/domain/graph/resolve.js +1 -1
  104. package/dist/domain/graph/resolve.js.map +1 -1
  105. package/dist/domain/graph/watcher.d.ts +2 -0
  106. package/dist/domain/graph/watcher.d.ts.map +1 -1
  107. package/dist/domain/graph/watcher.js +170 -75
  108. package/dist/domain/graph/watcher.js.map +1 -1
  109. package/dist/domain/parser.d.ts +3 -4
  110. package/dist/domain/parser.d.ts.map +1 -1
  111. package/dist/domain/parser.js +141 -89
  112. package/dist/domain/parser.js.map +1 -1
  113. package/dist/domain/search/generator.js +1 -1
  114. package/dist/domain/search/generator.js.map +1 -1
  115. package/dist/domain/search/models.d.ts +4 -3
  116. package/dist/domain/search/models.d.ts.map +1 -1
  117. package/dist/domain/search/models.js +23 -8
  118. package/dist/domain/search/models.js.map +1 -1
  119. package/dist/domain/search/search/hybrid.d.ts.map +1 -1
  120. package/dist/domain/search/search/hybrid.js +29 -18
  121. package/dist/domain/search/search/hybrid.js.map +1 -1
  122. package/dist/extractors/go.js +36 -33
  123. package/dist/extractors/go.js.map +1 -1
  124. package/dist/extractors/helpers.d.ts.map +1 -1
  125. package/dist/extractors/helpers.js +40 -29
  126. package/dist/extractors/helpers.js.map +1 -1
  127. package/dist/extractors/java.js +58 -46
  128. package/dist/extractors/java.js.map +1 -1
  129. package/dist/extractors/javascript.js +65 -54
  130. package/dist/extractors/javascript.js.map +1 -1
  131. package/dist/extractors/kotlin.js +84 -78
  132. package/dist/extractors/kotlin.js.map +1 -1
  133. package/dist/extractors/python.js +29 -24
  134. package/dist/extractors/python.js.map +1 -1
  135. package/dist/extractors/rust.js +41 -32
  136. package/dist/extractors/rust.js.map +1 -1
  137. package/dist/extractors/solidity.js +58 -67
  138. package/dist/extractors/solidity.js.map +1 -1
  139. package/dist/extractors/swift.js +83 -81
  140. package/dist/extractors/swift.js.map +1 -1
  141. package/dist/extractors/zig.js +58 -60
  142. package/dist/extractors/zig.js.map +1 -1
  143. package/dist/features/ast.d.ts +16 -14
  144. package/dist/features/ast.d.ts.map +1 -1
  145. package/dist/features/ast.js +83 -81
  146. package/dist/features/ast.js.map +1 -1
  147. package/dist/features/audit.d.ts.map +1 -1
  148. package/dist/features/audit.js +8 -6
  149. package/dist/features/audit.js.map +1 -1
  150. package/dist/features/branch-compare.d.ts.map +1 -1
  151. package/dist/features/branch-compare.js +69 -72
  152. package/dist/features/branch-compare.js.map +1 -1
  153. package/dist/features/communities.d.ts.map +1 -1
  154. package/dist/features/communities.js +19 -7
  155. package/dist/features/communities.js.map +1 -1
  156. package/dist/features/complexity.d.ts.map +1 -1
  157. package/dist/features/complexity.js +120 -125
  158. package/dist/features/complexity.js.map +1 -1
  159. package/dist/features/dataflow.d.ts.map +1 -1
  160. package/dist/features/dataflow.js +136 -137
  161. package/dist/features/dataflow.js.map +1 -1
  162. package/dist/features/flow.d.ts.map +1 -1
  163. package/dist/features/flow.js +84 -79
  164. package/dist/features/flow.js.map +1 -1
  165. package/dist/features/structure-query.d.ts.map +1 -1
  166. package/dist/features/structure-query.js +69 -65
  167. package/dist/features/structure-query.js.map +1 -1
  168. package/dist/graph/algorithms/leiden/optimiser.d.ts.map +1 -1
  169. package/dist/graph/algorithms/leiden/optimiser.js +70 -55
  170. package/dist/graph/algorithms/leiden/optimiser.js.map +1 -1
  171. package/dist/graph/algorithms/leiden/partition.d.ts.map +1 -1
  172. package/dist/graph/algorithms/leiden/partition.js +288 -266
  173. package/dist/graph/algorithms/leiden/partition.js.map +1 -1
  174. package/dist/graph/model.d.ts.map +1 -1
  175. package/dist/graph/model.js +5 -1
  176. package/dist/graph/model.js.map +1 -1
  177. package/dist/infrastructure/config.d.ts.map +1 -1
  178. package/dist/infrastructure/config.js +6 -4
  179. package/dist/infrastructure/config.js.map +1 -1
  180. package/dist/infrastructure/suppress.d.ts +25 -0
  181. package/dist/infrastructure/suppress.d.ts.map +1 -0
  182. package/dist/infrastructure/suppress.js +43 -0
  183. package/dist/infrastructure/suppress.js.map +1 -0
  184. package/dist/mcp/server.d.ts.map +1 -1
  185. package/dist/mcp/server.js +29 -24
  186. package/dist/mcp/server.js.map +1 -1
  187. package/dist/presentation/dataflow.d.ts.map +1 -1
  188. package/dist/presentation/dataflow.js +47 -38
  189. package/dist/presentation/dataflow.js.map +1 -1
  190. package/dist/presentation/diff-impact-mermaid.d.ts.map +1 -1
  191. package/dist/presentation/diff-impact-mermaid.js +60 -51
  192. package/dist/presentation/diff-impact-mermaid.js.map +1 -1
  193. package/dist/presentation/queries-cli/exports.d.ts.map +1 -1
  194. package/dist/presentation/queries-cli/exports.js +20 -14
  195. package/dist/presentation/queries-cli/exports.js.map +1 -1
  196. package/dist/presentation/queries-cli/impact.d.ts.map +1 -1
  197. package/dist/presentation/queries-cli/impact.js +15 -13
  198. package/dist/presentation/queries-cli/impact.js.map +1 -1
  199. package/dist/presentation/queries-cli/inspect.d.ts.map +1 -1
  200. package/dist/presentation/queries-cli/inspect.js +101 -79
  201. package/dist/presentation/queries-cli/inspect.js.map +1 -1
  202. package/dist/presentation/queries-cli/overview.d.ts.map +1 -1
  203. package/dist/presentation/queries-cli/overview.js +25 -16
  204. package/dist/presentation/queries-cli/overview.js.map +1 -1
  205. package/dist/presentation/queries-cli/path.js +26 -20
  206. package/dist/presentation/queries-cli/path.js.map +1 -1
  207. package/dist/presentation/result-formatter.d.ts +10 -0
  208. package/dist/presentation/result-formatter.d.ts.map +1 -1
  209. package/dist/presentation/result-formatter.js +16 -1
  210. package/dist/presentation/result-formatter.js.map +1 -1
  211. package/dist/presentation/viewer.d.ts.map +1 -1
  212. package/dist/presentation/viewer.js +18 -12
  213. package/dist/presentation/viewer.js.map +1 -1
  214. package/dist/shared/errors.d.ts +5 -0
  215. package/dist/shared/errors.d.ts.map +1 -1
  216. package/dist/shared/errors.js +5 -0
  217. package/dist/shared/errors.js.map +1 -1
  218. package/dist/shared/hierarchy.d.ts +8 -2
  219. package/dist/shared/hierarchy.d.ts.map +1 -1
  220. package/dist/shared/hierarchy.js +42 -1
  221. package/dist/shared/hierarchy.js.map +1 -1
  222. package/dist/shared/normalize.d.ts +6 -1
  223. package/dist/shared/normalize.d.ts.map +1 -1
  224. package/dist/shared/normalize.js +20 -12
  225. package/dist/shared/normalize.js.map +1 -1
  226. package/dist/shared/paginate.d.ts +0 -9
  227. package/dist/shared/paginate.d.ts.map +1 -1
  228. package/dist/shared/paginate.js +0 -15
  229. package/dist/shared/paginate.js.map +1 -1
  230. package/dist/types.d.ts +12 -5
  231. package/dist/types.d.ts.map +1 -1
  232. package/grammars/tree-sitter-erlang.wasm +0 -0
  233. package/grammars/tree-sitter-gleam.wasm +0 -0
  234. package/package.json +9 -9
  235. package/src/ast-analysis/engine.ts +176 -104
  236. package/src/ast-analysis/metrics.ts +33 -11
  237. package/src/ast-analysis/shared.ts +33 -24
  238. package/src/ast-analysis/visitor-utils.ts +52 -32
  239. package/src/ast-analysis/visitor.ts +132 -71
  240. package/src/ast-analysis/visitors/ast-store-visitor.ts +53 -50
  241. package/src/ast-analysis/visitors/complexity-visitor.ts +89 -40
  242. package/src/ast-analysis/visitors/dataflow-visitor.ts +87 -43
  243. package/src/cli/commands/branch-compare.ts +4 -0
  244. package/src/cli/commands/diff-impact.ts +2 -1
  245. package/src/cli/commands/info.ts +3 -2
  246. package/src/cli/commands/watch.ts +16 -2
  247. package/src/db/connection.ts +29 -28
  248. package/src/db/query-builder.ts +15 -3
  249. package/src/db/repository/base.ts +34 -0
  250. package/src/db/repository/native-repository.ts +104 -1
  251. package/src/db/repository/nodes.ts +13 -8
  252. package/src/db/repository/sqlite-repository.ts +55 -0
  253. package/src/domain/analysis/brief.ts +15 -25
  254. package/src/domain/analysis/context.ts +17 -10
  255. package/src/domain/analysis/dependencies.ts +77 -81
  256. package/src/domain/analysis/fn-impact.ts +36 -43
  257. package/src/domain/analysis/implementations.ts +11 -17
  258. package/src/domain/analysis/module-map.ts +58 -92
  259. package/src/domain/analysis/query-helpers.ts +18 -1
  260. package/src/domain/graph/builder/pipeline.ts +409 -99
  261. package/src/domain/graph/builder/stages/build-edges.ts +45 -19
  262. package/src/domain/graph/builder/stages/detect-changes.ts +2 -2
  263. package/src/domain/graph/builder/stages/finalize.ts +2 -2
  264. package/src/domain/graph/builder/stages/insert-nodes.ts +59 -34
  265. package/src/domain/graph/builder/stages/resolve-imports.ts +122 -100
  266. package/src/domain/graph/cycles.ts +110 -23
  267. package/src/domain/graph/resolve.ts +1 -1
  268. package/src/domain/graph/watcher.ts +202 -96
  269. package/src/domain/parser.ts +143 -89
  270. package/src/domain/search/generator.ts +1 -1
  271. package/src/domain/search/models.ts +26 -7
  272. package/src/domain/search/search/hybrid.ts +69 -51
  273. package/src/extractors/go.ts +43 -33
  274. package/src/extractors/helpers.ts +37 -23
  275. package/src/extractors/java.ts +66 -47
  276. package/src/extractors/javascript.ts +66 -54
  277. package/src/extractors/kotlin.ts +84 -77
  278. package/src/extractors/python.ts +31 -25
  279. package/src/extractors/rust.ts +37 -29
  280. package/src/extractors/solidity.ts +57 -61
  281. package/src/extractors/swift.ts +81 -80
  282. package/src/extractors/zig.ts +58 -61
  283. package/src/features/ast.ts +130 -110
  284. package/src/features/audit.ts +8 -6
  285. package/src/features/branch-compare.ts +105 -79
  286. package/src/features/communities.ts +25 -10
  287. package/src/features/complexity.ts +171 -134
  288. package/src/features/dataflow.ts +165 -175
  289. package/src/features/flow.ts +129 -92
  290. package/src/features/structure-query.ts +79 -64
  291. package/src/graph/algorithms/leiden/optimiser.ts +99 -55
  292. package/src/graph/algorithms/leiden/partition.ts +359 -294
  293. package/src/graph/model.ts +6 -1
  294. package/src/infrastructure/config.ts +6 -4
  295. package/src/infrastructure/suppress.ts +47 -0
  296. package/src/mcp/server.ts +53 -37
  297. package/src/presentation/dataflow.ts +50 -44
  298. package/src/presentation/diff-impact-mermaid.ts +104 -62
  299. package/src/presentation/queries-cli/exports.ts +21 -13
  300. package/src/presentation/queries-cli/impact.ts +15 -13
  301. package/src/presentation/queries-cli/inspect.ts +100 -81
  302. package/src/presentation/queries-cli/overview.ts +26 -16
  303. package/src/presentation/queries-cli/path.ts +33 -25
  304. package/src/presentation/result-formatter.ts +19 -1
  305. package/src/presentation/viewer.ts +42 -14
  306. package/src/shared/errors.ts +6 -0
  307. package/src/shared/hierarchy.ts +50 -2
  308. package/src/shared/normalize.ts +31 -12
  309. package/src/shared/paginate.ts +0 -17
  310. package/src/types.ts +26 -5
@@ -294,99 +294,107 @@ export function children(name: string, customDbPath: string, opts: OutputOpts =
294
294
  }
295
295
  }
296
296
 
297
- function renderContextResult(r: ContextResult): void {
298
- const lineRange = r.endLine ? `${r.line}-${r.endLine}` : `${r.line}`;
299
- const roleTag = r.role ? ` [${r.role}]` : '';
300
- console.log(`\n# ${r.name} (${r.kind})${roleTag} — ${r.file}:${lineRange}\n`);
297
+ function renderSignature(sig: ContextResult['signature']): void {
298
+ if (!sig) return;
299
+ console.log('## Type/Shape Info');
300
+ if (sig.params != null) console.log(` Parameters: (${sig.params})`);
301
+ if (sig.returnType) console.log(` Returns: ${sig.returnType}`);
302
+ console.log();
303
+ }
301
304
 
302
- if (r.signature) {
303
- console.log('## Type/Shape Info');
304
- if (r.signature.params != null) console.log(` Parameters: (${r.signature.params})`);
305
- if (r.signature.returnType) console.log(` Returns: ${r.signature.returnType}`);
306
- console.log();
305
+ function renderComplexity(cx: NonNullable<ContextResult['complexity']>): void {
306
+ const miPart = cx.maintainabilityIndex ? ` | MI: ${cx.maintainabilityIndex}` : '';
307
+ console.log('## Complexity');
308
+ console.log(
309
+ ` Cognitive: ${cx.cognitive} | Cyclomatic: ${cx.cyclomatic} | Max Nesting: ${cx.maxNesting}${miPart}`,
310
+ );
311
+ console.log();
312
+ }
313
+
314
+ function renderSource(source: string, indent = ' '): void {
315
+ console.log('## Source');
316
+ for (const line of source.split('\n')) {
317
+ console.log(`${indent}${line}`);
307
318
  }
319
+ console.log();
320
+ }
308
321
 
309
- if (r.children && r.children.length > 0) {
310
- console.log(`## Children (${r.children.length})`);
311
- for (const c of r.children) {
312
- console.log(` ${kindIcon(c.kind)} ${c.name} :${c.line}`);
322
+ function renderCallees(callees: CalleeRef[]): void {
323
+ if (callees.length === 0) return;
324
+ console.log(`## Direct Dependencies (${callees.length})`);
325
+ for (const c of callees) {
326
+ const summary = c.summary ? ` — ${c.summary}` : '';
327
+ console.log(` ${kindIcon(c.kind)} ${c.name} ${c.file}:${c.line}${summary}`);
328
+ if (c.source) {
329
+ const maxSourceLines = 10;
330
+ for (const line of c.source.split('\n').slice(0, maxSourceLines)) {
331
+ console.log(` | ${line}`);
332
+ }
313
333
  }
314
- console.log();
315
334
  }
335
+ console.log();
336
+ }
316
337
 
317
- if (r.complexity) {
318
- const cx = r.complexity;
319
- const miPart = cx.maintainabilityIndex ? ` | MI: ${cx.maintainabilityIndex}` : '';
320
- console.log('## Complexity');
321
- console.log(
322
- ` Cognitive: ${cx.cognitive} | Cyclomatic: ${cx.cyclomatic} | Max Nesting: ${cx.maxNesting}${miPart}`,
323
- );
324
- console.log();
338
+ function renderCallers(callers: CallerRef[]): void {
339
+ if (callers.length === 0) return;
340
+ console.log(`## Callers (${callers.length})`);
341
+ for (const c of callers) {
342
+ const via = c.viaHierarchy ? ` (via ${c.viaHierarchy})` : '';
343
+ console.log(` ${kindIcon(c.kind)} ${c.name} ${c.file}:${c.line}${via}`);
325
344
  }
345
+ console.log();
346
+ }
326
347
 
327
- if (r.source) {
328
- console.log('## Source');
329
- for (const line of r.source.split('\n')) {
330
- console.log(` ${line}`);
331
- }
332
- console.log();
348
+ function renderSymbolRefList(label: string, items: SymbolRef[]): void {
349
+ if (items.length === 0) return;
350
+ console.log(`## ${label} (${items.length})`);
351
+ for (const s of items) {
352
+ console.log(` ${kindIcon(s.kind)} ${s.name} ${s.file}:${s.line}`);
333
353
  }
354
+ console.log();
355
+ }
334
356
 
335
- if (r.callees.length > 0) {
336
- console.log(`## Direct Dependencies (${r.callees.length})`);
337
- for (const c of r.callees) {
338
- const summary = c.summary ? ` — ${c.summary}` : '';
339
- console.log(` ${kindIcon(c.kind)} ${c.name} ${c.file}:${c.line}${summary}`);
340
- if (c.source) {
341
- for (const line of c.source.split('\n').slice(0, 10)) {
342
- console.log(` | ${line}`);
343
- }
357
+ function renderRelatedTests(tests: ContextResult['relatedTests']): void {
358
+ if (tests.length === 0) return;
359
+ console.log('## Related Tests');
360
+ const maxTestSourceLines = 20;
361
+ for (const t of tests) {
362
+ console.log(` ${t.file} — ${t.testCount} tests`);
363
+ for (const tn of t.testNames) {
364
+ console.log(` - ${tn}`);
365
+ }
366
+ if (t.source) {
367
+ console.log(' Source:');
368
+ for (const line of t.source.split('\n').slice(0, maxTestSourceLines)) {
369
+ console.log(` | ${line}`);
344
370
  }
345
371
  }
346
- console.log();
347
372
  }
373
+ console.log();
374
+ }
348
375
 
349
- if (r.callers.length > 0) {
350
- console.log(`## Callers (${r.callers.length})`);
351
- for (const c of r.callers) {
352
- const via = c.viaHierarchy ? ` (via ${c.viaHierarchy})` : '';
353
- console.log(` ${kindIcon(c.kind)} ${c.name} ${c.file}:${c.line}${via}`);
354
- }
355
- console.log();
356
- }
376
+ function renderContextResult(r: ContextResult): void {
377
+ const lineRange = r.endLine ? `${r.line}-${r.endLine}` : `${r.line}`;
378
+ const roleTag = r.role ? ` [${r.role}]` : '';
379
+ console.log(`\n# ${r.name} (${r.kind})${roleTag} ${r.file}:${lineRange}\n`);
357
380
 
358
- if (r.implementors && r.implementors.length > 0) {
359
- console.log(`## Implementors (${r.implementors.length})`);
360
- for (const impl of r.implementors) {
361
- console.log(` ${kindIcon(impl.kind)} ${impl.name} ${impl.file}:${impl.line}`);
362
- }
363
- console.log();
364
- }
381
+ renderSignature(r.signature);
365
382
 
366
- if (r.implements && r.implements.length > 0) {
367
- console.log(`## Implements (${r.implements.length})`);
368
- for (const iface of r.implements) {
369
- console.log(` ${kindIcon(iface.kind)} ${iface.name} ${iface.file}:${iface.line}`);
383
+ if (r.children && r.children.length > 0) {
384
+ console.log(`## Children (${r.children.length})`);
385
+ for (const c of r.children) {
386
+ console.log(` ${kindIcon(c.kind)} ${c.name} :${c.line}`);
370
387
  }
371
388
  console.log();
372
389
  }
373
390
 
374
- if (r.relatedTests.length > 0) {
375
- console.log('## Related Tests');
376
- for (const t of r.relatedTests) {
377
- console.log(` ${t.file} — ${t.testCount} tests`);
378
- for (const tn of t.testNames) {
379
- console.log(` - ${tn}`);
380
- }
381
- if (t.source) {
382
- console.log(' Source:');
383
- for (const line of t.source.split('\n').slice(0, 20)) {
384
- console.log(` | ${line}`);
385
- }
386
- }
387
- }
388
- console.log();
389
- }
391
+ if (r.complexity) renderComplexity(r.complexity);
392
+ if (r.source) renderSource(r.source);
393
+ renderCallees(r.callees);
394
+ renderCallers(r.callers);
395
+ renderSymbolRefList('Implementors', r.implementors || []);
396
+ renderSymbolRefList('Implements', r.implements || []);
397
+ renderRelatedTests(r.relatedTests);
390
398
 
391
399
  if (r.callees.length === 0 && r.callers.length === 0 && r.relatedTests.length === 0) {
392
400
  console.log(' (no call edges or tests found — may be invoked dynamically or via re-exports)');
@@ -439,7 +447,7 @@ function renderFileExplain(r: FileExplainResult): void {
439
447
  console.log();
440
448
  }
441
449
 
442
- function renderFunctionExplain(r: FunctionExplainResult, indent = ''): void {
450
+ function renderExplainHeader(r: FunctionExplainResult, indent: string): void {
443
451
  const lineRange = r.endLine ? `${r.line}-${r.endLine}` : `${r.line}`;
444
452
  const lineInfo = r.lineCount ? `${r.lineCount} lines` : '';
445
453
  const summaryPart = r.summary ? ` | ${r.summary}` : '';
@@ -454,15 +462,19 @@ function renderFunctionExplain(r: FunctionExplainResult, indent = ''): void {
454
462
  if (r.signature.params != null) console.log(`${indent} Parameters: (${r.signature.params})`);
455
463
  if (r.signature.returnType) console.log(`${indent} Returns: ${r.signature.returnType}`);
456
464
  }
465
+ }
457
466
 
458
- if (r.complexity) {
459
- const cx = r.complexity;
460
- const miPart = cx.maintainabilityIndex ? ` MI=${cx.maintainabilityIndex}` : '';
461
- console.log(
462
- `${indent} Complexity: cognitive=${cx.cognitive} cyclomatic=${cx.cyclomatic} nesting=${cx.maxNesting}${miPart}`,
463
- );
464
- }
467
+ function renderExplainComplexity(
468
+ cx: NonNullable<FunctionExplainResult['complexity']>,
469
+ indent: string,
470
+ ): void {
471
+ const miPart = cx.maintainabilityIndex ? ` MI=${cx.maintainabilityIndex}` : '';
472
+ console.log(
473
+ `${indent} Complexity: cognitive=${cx.cognitive} cyclomatic=${cx.cyclomatic} nesting=${cx.maxNesting}${miPart}`,
474
+ );
475
+ }
465
476
 
477
+ function renderExplainEdges(r: FunctionExplainResult, indent: string): void {
466
478
  if (r.callees.length > 0) {
467
479
  console.log(`\n${indent} Calls (${r.callees.length}):`);
468
480
  for (const c of r.callees) {
@@ -488,8 +500,15 @@ function renderFunctionExplain(r: FunctionExplainResult, indent = ''): void {
488
500
  if (r.callees.length === 0 && r.callers.length === 0) {
489
501
  console.log(`${indent} (no call edges found -- may be invoked dynamically or via re-exports)`);
490
502
  }
503
+ }
504
+
505
+ function renderFunctionExplain(r: FunctionExplainResult, indent = ''): void {
506
+ renderExplainHeader(r, indent);
507
+ if (r.complexity) renderExplainComplexity(r.complexity, indent);
508
+ renderExplainEdges(r, indent);
491
509
 
492
510
  if (r.depDetails && r.depDetails.length > 0) {
511
+ const depthLevel = r._depth || 0;
493
512
  console.log(`\n${indent} --- Dependencies (depth ${depthLevel + 1}) ---`);
494
513
  for (const dep of r.depDetails) {
495
514
  renderFunctionExplain(dep, `${indent} `);
@@ -1,6 +1,8 @@
1
1
  import path from 'node:path';
2
2
  import { kindIcon, moduleMapData, rolesData, statsData } from '../../domain/queries.js';
3
+ import { debug } from '../../infrastructure/logger.js';
3
4
  import { outputResult } from '../../infrastructure/result-formatter.js';
5
+ import { toErrorMessage } from '../../shared/errors.js';
4
6
 
5
7
  interface OutputOpts {
6
8
  json?: boolean;
@@ -236,8 +238,8 @@ export async function stats(customDbPath: string, opts: OutputOpts = {}): Promis
236
238
  try {
237
239
  const { communitySummaryForStats } = await import('../../features/communities.js');
238
240
  data.communities = communitySummaryForStats(customDbPath, { noTests: opts.noTests });
239
- } catch {
240
- /* community detection is optional; silently skip on any error */
241
+ } catch (e) {
242
+ debug(`stats: community detection failed (optional): ${toErrorMessage(e)}`);
241
243
  }
242
244
 
243
245
  if (outputResult(data as unknown as Record<string, unknown>, null, opts)) return;
@@ -281,6 +283,26 @@ export function moduleMap(customDbPath: string, limit = 20, opts: OutputOpts = {
281
283
  );
282
284
  }
283
285
 
286
+ function groupByRole(symbols: RoleSymbol[]): Record<string, RoleSymbol[]> {
287
+ const byRole: Record<string, RoleSymbol[]> = {};
288
+ for (const s of symbols) {
289
+ if (!byRole[s.role]) byRole[s.role] = [];
290
+ byRole[s.role]!.push(s);
291
+ }
292
+ return byRole;
293
+ }
294
+
295
+ function printRoleGroup(role: string, symbols: RoleSymbol[]): void {
296
+ console.log(`## ${role} (${symbols.length})`);
297
+ for (const s of symbols.slice(0, 30)) {
298
+ console.log(` ${kindIcon(s.kind)} ${s.name} ${s.file}:${s.line}`);
299
+ }
300
+ if (symbols.length > 30) {
301
+ console.log(` ... and ${symbols.length - 30} more`);
302
+ }
303
+ console.log();
304
+ }
305
+
284
306
  export function roles(customDbPath: string, opts: OutputOpts = {}): void {
285
307
  const data = rolesData(customDbPath, opts) as RolesData;
286
308
  if (outputResult(data as unknown as Record<string, unknown>, 'symbols', opts)) return;
@@ -298,20 +320,8 @@ export function roles(customDbPath: string, opts: OutputOpts = {}): void {
298
320
  .map(([role, count]) => `${role}: ${count}`);
299
321
  console.log(` ${summaryParts.join(' ')}\n`);
300
322
 
301
- const byRole: Record<string, RoleSymbol[]> = {};
302
- for (const s of data.symbols) {
303
- if (!byRole[s.role]) byRole[s.role] = [];
304
- byRole[s.role]!.push(s);
305
- }
306
-
323
+ const byRole = groupByRole(data.symbols);
307
324
  for (const [role, symbols] of Object.entries(byRole)) {
308
- console.log(`## ${role} (${symbols.length})`);
309
- for (const s of symbols.slice(0, 30)) {
310
- console.log(` ${kindIcon(s.kind)} ${s.name} ${s.file}:${s.line}`);
311
- }
312
- if (symbols.length > 30) {
313
- console.log(` ... and ${symbols.length - 30} more`);
314
- }
315
- console.log();
325
+ printRoleGroup(role, symbols);
316
326
  }
317
327
  }
@@ -127,6 +127,37 @@ interface FilePathDataResult {
127
127
  alternateCount: number;
128
128
  }
129
129
 
130
+ function printFilePathNotFound(from: string, to: string, data: FilePathDataResult): void {
131
+ const dir = data.reverse ? 'reverse ' : '';
132
+ console.log(`No ${dir}file path from "${from}" to "${to}" within ${data.maxDepth} hops.`);
133
+ if (data.fromCandidates.length > 1) {
134
+ console.log(
135
+ `\n "${from}" matched ${data.fromCandidates.length} files — using: ${data.fromCandidates[0]}`,
136
+ );
137
+ }
138
+ if (data.toCandidates.length > 1) {
139
+ console.log(
140
+ ` "${to}" matched ${data.toCandidates.length} files — using: ${data.toCandidates[0]}`,
141
+ );
142
+ }
143
+ }
144
+
145
+ function printFilePathSteps(data: FilePathDataResult): void {
146
+ for (let i = 0; i < data.path.length; i++) {
147
+ const indent = ' '.repeat(i + 1);
148
+ if (i === 0) {
149
+ console.log(`${indent}${data.path[i]}`);
150
+ } else {
151
+ console.log(`${indent}→ ${data.path[i]}`);
152
+ }
153
+ }
154
+ if (data.alternateCount > 0) {
155
+ console.log(
156
+ `\n (${data.alternateCount} alternate shortest ${data.alternateCount === 1 ? 'path' : 'paths'} at same depth)`,
157
+ );
158
+ }
159
+ }
160
+
130
161
  function filePath(from: string, to: string, customDbPath: string, opts: PathOpts = {}): void {
131
162
  const data = filePathData(from, to, customDbPath, opts) as FilePathDataResult;
132
163
  if (outputResult(data as unknown as Record<string, unknown>, null, opts)) return;
@@ -137,18 +168,7 @@ function filePath(from: string, to: string, customDbPath: string, opts: PathOpts
137
168
  }
138
169
 
139
170
  if (!data.found) {
140
- const dir = data.reverse ? 'reverse ' : '';
141
- console.log(`No ${dir}file path from "${from}" to "${to}" within ${data.maxDepth} hops.`);
142
- if (data.fromCandidates.length > 1) {
143
- console.log(
144
- `\n "${from}" matched ${data.fromCandidates.length} files — using: ${data.fromCandidates[0]}`,
145
- );
146
- }
147
- if (data.toCandidates.length > 1) {
148
- console.log(
149
- ` "${to}" matched ${data.toCandidates.length} files — using: ${data.toCandidates[0]}`,
150
- );
151
- }
171
+ printFilePathNotFound(from, to, data);
152
172
  return;
153
173
  }
154
174
 
@@ -162,18 +182,6 @@ function filePath(from: string, to: string, customDbPath: string, opts: PathOpts
162
182
  console.log(
163
183
  `\nFile path from ${from} to ${to} (${data.hops} ${data.hops === 1 ? 'hop' : 'hops'})${dir}:\n`,
164
184
  );
165
- for (let i = 0; i < data.path.length; i++) {
166
- const indent = ' '.repeat(i + 1);
167
- if (i === 0) {
168
- console.log(`${indent}${data.path[i]}`);
169
- } else {
170
- console.log(`${indent}→ ${data.path[i]}`);
171
- }
172
- }
173
- if (data.alternateCount > 0) {
174
- console.log(
175
- `\n (${data.alternateCount} alternate shortest ${data.alternateCount === 1 ? 'path' : 'paths'} at same depth)`,
176
- );
177
- }
185
+ printFilePathSteps(data);
178
186
  console.log();
179
187
  }
@@ -1,7 +1,25 @@
1
1
  import { loadConfig } from '../infrastructure/config.js';
2
- import { printNdjson } from '../shared/paginate.js';
2
+ import type { PaginationMeta } from '../shared/paginate.js';
3
3
  import { formatTable, truncEnd } from './table.js';
4
4
 
5
+ /**
6
+ * Print data as newline-delimited JSON (NDJSON).
7
+ *
8
+ * Emits a `_meta` line with pagination info (if present), then one JSON
9
+ * line per item in the named array field.
10
+ */
11
+ export function printNdjson(
12
+ data: Record<string, unknown> & { _pagination?: PaginationMeta },
13
+ field: string,
14
+ ): void {
15
+ if (data._pagination) console.log(JSON.stringify({ _meta: data._pagination }));
16
+ const items = data[field];
17
+ if (Array.isArray(items)) {
18
+ // console.log is intentional: NDJSON serialisation must go to stdout, not the structured logger
19
+ for (const item of items) console.log(JSON.stringify(item));
20
+ }
21
+ }
22
+
5
23
  function flattenObject(obj: Record<string, unknown>, prefix = ''): Record<string, unknown> {
6
24
  const result: Record<string, unknown> = {};
7
25
  for (const [key, value] of Object.entries(obj)) {
@@ -1,5 +1,7 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
+ import { debug } from '../infrastructure/logger.js';
4
+ import { toErrorMessage } from '../shared/errors.js';
3
5
  import { COMMUNITY_COLORS, DEFAULT_NODE_COLORS, DEFAULT_ROLE_COLORS } from './colors.js';
4
6
 
5
7
  // Re-export color constants so existing consumers are unaffected
@@ -74,8 +76,8 @@ export function loadPlotConfig(dir: string): PlotConfig {
74
76
  ...(raw.riskThresholds || {}),
75
77
  },
76
78
  };
77
- } catch {
78
- // Invalid JSON use defaults
79
+ } catch (e) {
80
+ debug(`loadViewerConfig: invalid JSON in config file: ${toErrorMessage(e)}`);
79
81
  }
80
82
  }
81
83
  }
@@ -179,19 +181,45 @@ export interface ViewerData {
179
181
  seedNodeIds: (number | string)[];
180
182
  }
181
183
 
184
+ interface ResolvedPlotConfig {
185
+ layoutOpts: LayoutOptions;
186
+ title: string;
187
+ layoutAlgorithm: string;
188
+ layoutDirection: string;
189
+ physicsEnabled: boolean;
190
+ sizeBy: string;
191
+ clusterBy: string;
192
+ effectiveColorBy: string;
193
+ effectiveRisk: boolean;
194
+ }
195
+
196
+ function resolvePlotConfig(cfg: PlotConfig): ResolvedPlotConfig {
197
+ return {
198
+ layoutOpts: buildLayoutOptions(cfg),
199
+ title: cfg.title || 'Codegraph',
200
+ layoutAlgorithm: cfg.layout?.algorithm || 'hierarchical',
201
+ layoutDirection: cfg.layout?.direction || 'LR',
202
+ physicsEnabled: cfg.physics?.enabled !== false,
203
+ sizeBy: cfg.sizeBy || 'uniform',
204
+ clusterBy: cfg.clusterBy || 'none',
205
+ effectiveColorBy:
206
+ cfg.overlays?.complexity && cfg.colorBy === 'kind' ? 'complexity' : cfg.colorBy || 'kind',
207
+ effectiveRisk: cfg.overlays?.risk || false,
208
+ };
209
+ }
210
+
182
211
  export function renderPlotHTML(data: ViewerData, cfg: PlotConfig): string {
183
- const layoutOpts = buildLayoutOptions(cfg);
184
- const title = cfg.title || 'Codegraph';
185
-
186
- // Resolve effective colorBy (overlays.complexity overrides)
187
- const layoutAlgorithm = cfg.layout?.algorithm || 'hierarchical';
188
- const layoutDirection = cfg.layout?.direction || 'LR';
189
- const physicsEnabled = cfg.physics?.enabled !== false;
190
- const sizeBy = cfg.sizeBy || 'uniform';
191
- const clusterBy = cfg.clusterBy || 'none';
192
- const effectiveColorBy =
193
- cfg.overlays?.complexity && cfg.colorBy === 'kind' ? 'complexity' : cfg.colorBy || 'kind';
194
- const effectiveRisk = cfg.overlays?.risk || false;
212
+ const {
213
+ layoutOpts,
214
+ title,
215
+ layoutAlgorithm,
216
+ layoutDirection,
217
+ physicsEnabled,
218
+ sizeBy,
219
+ clusterBy,
220
+ effectiveColorBy,
221
+ effectiveRisk,
222
+ } = resolvePlotConfig(cfg);
195
223
 
196
224
  return `<!DOCTYPE html>
197
225
  <html lang="en">
@@ -77,3 +77,9 @@ export class BoundaryError extends CodegraphError {
77
77
  export function toErrorMessage(e: unknown): string {
78
78
  return e instanceof Error ? e.message : String(e);
79
79
  }
80
+
81
+ /**
82
+ * Catch-suppression helpers (`suppressError`, `suppressErrorAsync`) live in
83
+ * `infrastructure/suppress.ts` to avoid a shared→infrastructure layer inversion.
84
+ * Import them from there instead of from this module.
85
+ */
@@ -1,10 +1,58 @@
1
1
  import { getClassHierarchy } from '../db/index.js';
2
- import type { BetterSqlite3Database, NodeRow } from '../types.js';
2
+ import type { BetterSqlite3Database, NodeRow, Repository } from '../types.js';
3
3
 
4
+ /**
5
+ * Resolve all methods in the class hierarchy that share the given method name.
6
+ *
7
+ * Accepts either a raw BetterSqlite3Database handle (legacy) or a Repository
8
+ * instance (preferred — works with both SqliteRepository and NativeRepository).
9
+ */
4
10
  export function resolveMethodViaHierarchy(
5
- db: BetterSqlite3Database,
11
+ dbOrRepo: BetterSqlite3Database | Repository,
6
12
  methodName: string,
7
13
  ): NodeRow[] {
14
+ // Detect Repository vs raw DB by duck-typing on findNodesWithFanIn
15
+ if (
16
+ typeof (dbOrRepo as Repository).findNodesWithFanIn === 'function' &&
17
+ typeof (dbOrRepo as Repository).getClassHierarchy === 'function'
18
+ ) {
19
+ return resolveViaRepo(dbOrRepo as Repository, methodName);
20
+ }
21
+ return resolveViaRawDb(dbOrRepo as BetterSqlite3Database, methodName);
22
+ }
23
+
24
+ /** Strip fan_in from NodeRowWithFanIn to produce a plain NodeRow. */
25
+ function stripFanIn(rows: Array<{ fan_in: number } & NodeRow>): NodeRow[] {
26
+ return rows.map(({ fan_in: _, ...rest }) => rest);
27
+ }
28
+
29
+ function resolveViaRepo(repo: Repository, methodName: string): NodeRow[] {
30
+ const methods = stripFanIn(repo.findNodesWithFanIn(`%.${methodName}`, { kinds: ['method'] }));
31
+
32
+ const results: NodeRow[] = [...methods];
33
+ for (const m of methods) {
34
+ const className = m.name.split('.')[0]!;
35
+ const classNodes = repo.findNodesWithFanIn(className, {
36
+ kinds: ['class'],
37
+ file: m.file,
38
+ });
39
+ const classNode = classNodes[0];
40
+ if (!classNode) continue;
41
+
42
+ const ancestors = repo.getClassHierarchy(classNode.id);
43
+ for (const ancestorId of ancestors) {
44
+ const ancestor = repo.findNodeById(ancestorId);
45
+ if (!ancestor) continue;
46
+ const parentMethods = stripFanIn(
47
+ repo.findNodesWithFanIn(`${ancestor.name}.${methodName}`, { kinds: ['method'] }),
48
+ );
49
+ results.push(...parentMethods);
50
+ }
51
+ }
52
+ return results;
53
+ }
54
+
55
+ function resolveViaRawDb(db: BetterSqlite3Database, methodName: string): NodeRow[] {
8
56
  const methods = db
9
57
  .prepare(`SELECT * FROM nodes WHERE kind = 'method' AND name LIKE ?`)
10
58
  .all(`%.${methodName}`) as NodeRow[];
@@ -3,6 +3,15 @@ interface DbHandle {
3
3
  prepare(sql: string): { get(...params: unknown[]): unknown };
4
4
  }
5
5
 
6
+ /** Anything that can look up a file hash — either a raw DB or a Repository. */
7
+ interface HashSource {
8
+ getFileHash(file: string): string | null;
9
+ }
10
+
11
+ function isHashSource(x: unknown): x is HashSource {
12
+ return typeof x === 'object' && x !== null && typeof (x as HashSource).getFileHash === 'function';
13
+ }
14
+
6
15
  export function getFileHash(db: DbHandle, file: string): string | null {
7
16
  const row = db.prepare('SELECT hash FROM file_hashes WHERE file = ?').get(file) as
8
17
  | { hash: string }
@@ -55,25 +64,35 @@ interface RawSymbolRow {
55
64
  role?: string | null;
56
65
  }
57
66
 
67
+ /**
68
+ * Resolve a file hash, using the cache when available.
69
+ * Accepts a raw DB handle (with .prepare) or a Repository (with .getFileHash).
70
+ */
71
+ function resolveFileHash(
72
+ db: DbHandle | HashSource,
73
+ file: string,
74
+ hashCache?: Map<string, string | null>,
75
+ ): string | null {
76
+ const lookupHash = isHashSource(db)
77
+ ? (f: string) => db.getFileHash(f)
78
+ : (f: string) => getFileHash(db as DbHandle, f);
79
+ if (!hashCache) return lookupHash(file);
80
+ if (!hashCache.has(file)) {
81
+ hashCache.set(file, lookupHash(file));
82
+ }
83
+ return hashCache.get(file) ?? null;
84
+ }
85
+
58
86
  /**
59
87
  * Normalize a raw DB/query row into the stable 7-field symbol shape.
88
+ * Accepts a raw DB handle (with .prepare), a Repository (with .getFileHash), or null.
60
89
  */
61
90
  export function normalizeSymbol(
62
91
  row: RawSymbolRow,
63
- db?: DbHandle | null,
92
+ db?: DbHandle | HashSource | null,
64
93
  hashCache?: Map<string, string | null>,
65
94
  ): NormalizedSymbol {
66
- let fileHash: string | null = null;
67
- if (db) {
68
- if (hashCache) {
69
- if (!hashCache.has(row.file)) {
70
- hashCache.set(row.file, getFileHash(db, row.file));
71
- }
72
- fileHash = hashCache.get(row.file) ?? null;
73
- } else {
74
- fileHash = getFileHash(db, row.file);
75
- }
76
- }
95
+ const fileHash = db ? resolveFileHash(db, row.file, hashCache) : null;
77
96
  return {
78
97
  name: row.name,
79
98
  kind: row.kind,
@@ -105,20 +105,3 @@ export function paginateResult<T extends Record<string, unknown>>(
105
105
  const { items, pagination } = paginate(arr, { limit, offset });
106
106
  return { ...result, [field]: items, _pagination: pagination };
107
107
  }
108
-
109
- /**
110
- * Print data as newline-delimited JSON (NDJSON).
111
- *
112
- * Emits a `_meta` line with pagination info (if present), then one JSON
113
- * line per item in the named array field.
114
- */
115
- export function printNdjson(
116
- data: Record<string, unknown> & { _pagination?: PaginationMeta },
117
- field: string,
118
- ): void {
119
- if (data._pagination) console.log(JSON.stringify({ _meta: data._pagination }));
120
- const items = data[field];
121
- if (Array.isArray(items)) {
122
- for (const item of items) console.log(JSON.stringify(item));
123
- }
124
- }