@optave/codegraph 3.0.4 → 3.1.1

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 (49) hide show
  1. package/README.md +59 -52
  2. package/grammars/tree-sitter-go.wasm +0 -0
  3. package/package.json +9 -10
  4. package/src/ast-analysis/rules/csharp.js +201 -0
  5. package/src/ast-analysis/rules/go.js +182 -0
  6. package/src/ast-analysis/rules/index.js +82 -0
  7. package/src/ast-analysis/rules/java.js +175 -0
  8. package/src/ast-analysis/rules/javascript.js +246 -0
  9. package/src/ast-analysis/rules/php.js +219 -0
  10. package/src/ast-analysis/rules/python.js +196 -0
  11. package/src/ast-analysis/rules/ruby.js +204 -0
  12. package/src/ast-analysis/rules/rust.js +173 -0
  13. package/src/ast-analysis/shared.js +223 -0
  14. package/src/ast.js +15 -28
  15. package/src/audit.js +4 -5
  16. package/src/boundaries.js +1 -1
  17. package/src/branch-compare.js +84 -79
  18. package/src/builder.js +274 -159
  19. package/src/cfg.js +111 -341
  20. package/src/check.js +3 -3
  21. package/src/cli.js +122 -167
  22. package/src/cochange.js +1 -1
  23. package/src/communities.js +13 -16
  24. package/src/complexity.js +196 -1239
  25. package/src/cycles.js +1 -1
  26. package/src/dataflow.js +274 -697
  27. package/src/db/connection.js +88 -0
  28. package/src/db/migrations.js +312 -0
  29. package/src/db/query-builder.js +280 -0
  30. package/src/db/repository.js +134 -0
  31. package/src/db.js +19 -392
  32. package/src/embedder.js +145 -141
  33. package/src/export.js +1 -1
  34. package/src/flow.js +160 -228
  35. package/src/index.js +36 -2
  36. package/src/kinds.js +49 -0
  37. package/src/manifesto.js +3 -8
  38. package/src/mcp.js +97 -20
  39. package/src/owners.js +132 -132
  40. package/src/parser.js +58 -131
  41. package/src/queries-cli.js +866 -0
  42. package/src/queries.js +1356 -2261
  43. package/src/resolve.js +11 -2
  44. package/src/result-formatter.js +21 -0
  45. package/src/sequence.js +364 -0
  46. package/src/structure.js +200 -199
  47. package/src/test-filter.js +7 -0
  48. package/src/triage.js +120 -162
  49. package/src/viewer.js +1 -1
package/src/flow.js CHANGED
@@ -6,9 +6,11 @@
6
6
  */
7
7
 
8
8
  import { openReadonlyOrFail } from './db.js';
9
- import { paginateResult, printNdjson } from './paginate.js';
10
- import { isTestFile, kindIcon } from './queries.js';
9
+ import { paginateResult } from './paginate.js';
10
+ import { CORE_SYMBOL_KINDS, findMatchingNodes, kindIcon } from './queries.js';
11
+ import { outputResult } from './result-formatter.js';
11
12
  import { FRAMEWORK_ENTRY_PREFIXES } from './structure.js';
13
+ import { isTestFile } from './test-filter.js';
12
14
 
13
15
  /**
14
16
  * Determine the entry point type from a node name based on framework prefixes.
@@ -35,46 +37,49 @@ export function entryPointType(name) {
35
37
  */
36
38
  export function listEntryPointsData(dbPath, opts = {}) {
37
39
  const db = openReadonlyOrFail(dbPath);
38
- const noTests = opts.noTests || false;
39
-
40
- // Find all framework-prefixed nodes
41
- const prefixConditions = FRAMEWORK_ENTRY_PREFIXES.map(() => 'n.name LIKE ?').join(' OR ');
42
- const prefixParams = FRAMEWORK_ENTRY_PREFIXES.map((p) => `${p}%`);
43
-
44
- let rows = db
45
- .prepare(
46
- `SELECT n.name, n.kind, n.file, n.line, n.role
47
- FROM nodes n
48
- WHERE (
49
- (${prefixConditions})
50
- OR n.role = 'entry'
51
- )
52
- AND n.kind NOT IN ('file', 'directory')
53
- ORDER BY n.name`,
54
- )
55
- .all(...prefixParams);
56
-
57
- if (noTests) rows = rows.filter((r) => !isTestFile(r.file));
58
-
59
- const entries = rows.map((r) => ({
60
- name: r.name,
61
- kind: r.kind,
62
- file: r.file,
63
- line: r.line,
64
- role: r.role,
65
- type: entryPointType(r.name) || (r.role === 'entry' ? 'exported' : null),
66
- }));
67
-
68
- const byType = {};
69
- for (const e of entries) {
70
- const t = e.type || 'other';
71
- if (!byType[t]) byType[t] = [];
72
- byType[t].push(e);
73
- }
40
+ try {
41
+ const noTests = opts.noTests || false;
42
+
43
+ // Find all framework-prefixed nodes
44
+ const prefixConditions = FRAMEWORK_ENTRY_PREFIXES.map(() => 'n.name LIKE ?').join(' OR ');
45
+ const prefixParams = FRAMEWORK_ENTRY_PREFIXES.map((p) => `${p}%`);
74
46
 
75
- db.close();
76
- const base = { entries, byType, count: entries.length };
77
- return paginateResult(base, 'entries', { limit: opts.limit, offset: opts.offset });
47
+ let rows = db
48
+ .prepare(
49
+ `SELECT n.name, n.kind, n.file, n.line, n.role
50
+ FROM nodes n
51
+ WHERE (
52
+ (${prefixConditions})
53
+ OR n.role = 'entry'
54
+ )
55
+ AND n.kind NOT IN ('file', 'directory')
56
+ ORDER BY n.name`,
57
+ )
58
+ .all(...prefixParams);
59
+
60
+ if (noTests) rows = rows.filter((r) => !isTestFile(r.file));
61
+
62
+ const entries = rows.map((r) => ({
63
+ name: r.name,
64
+ kind: r.kind,
65
+ file: r.file,
66
+ line: r.line,
67
+ role: r.role,
68
+ type: entryPointType(r.name) || (r.role === 'entry' ? 'exported' : null),
69
+ }));
70
+
71
+ const byType = {};
72
+ for (const e of entries) {
73
+ const t = e.type || 'other';
74
+ if (!byType[t]) byType[t] = [];
75
+ byType[t].push(e);
76
+ }
77
+
78
+ const base = { entries, byType, count: entries.length };
79
+ return paginateResult(base, 'entries', { limit: opts.limit, offset: opts.offset });
80
+ } finally {
81
+ db.close();
82
+ }
78
83
  }
79
84
 
80
85
  /**
@@ -91,199 +96,136 @@ export function listEntryPointsData(dbPath, opts = {}) {
91
96
  */
92
97
  export function flowData(name, dbPath, opts = {}) {
93
98
  const db = openReadonlyOrFail(dbPath);
94
- const maxDepth = opts.depth || 10;
95
- const noTests = opts.noTests || false;
96
-
97
- // Phase 1: Direct LIKE match on full name
98
- let matchNode = findBestMatch(db, name, opts);
99
+ try {
100
+ const maxDepth = opts.depth || 10;
101
+ const noTests = opts.noTests || false;
102
+ const flowOpts = { ...opts, kinds: opts.kind ? [opts.kind] : CORE_SYMBOL_KINDS };
103
+
104
+ // Phase 1: Direct LIKE match on full name (use all 10 core symbol kinds,
105
+ // not just FUNCTION_KINDS, so flow can trace from interfaces/types/structs/etc.)
106
+ let matchNode = findMatchingNodes(db, name, flowOpts)[0] ?? null;
107
+
108
+ // Phase 2: Prefix-stripped matching — try adding framework prefixes
109
+ if (!matchNode) {
110
+ for (const prefix of FRAMEWORK_ENTRY_PREFIXES) {
111
+ matchNode = findMatchingNodes(db, `${prefix}${name}`, flowOpts)[0] ?? null;
112
+ if (matchNode) break;
113
+ }
114
+ }
99
115
 
100
- // Phase 2: Prefix-stripped matching — try adding framework prefixes
101
- if (!matchNode) {
102
- for (const prefix of FRAMEWORK_ENTRY_PREFIXES) {
103
- matchNode = findBestMatch(db, `${prefix}${name}`, opts);
104
- if (matchNode) break;
116
+ if (!matchNode) {
117
+ return {
118
+ entry: null,
119
+ depth: maxDepth,
120
+ steps: [],
121
+ leaves: [],
122
+ cycles: [],
123
+ totalReached: 0,
124
+ truncated: false,
125
+ };
105
126
  }
106
- }
107
127
 
108
- if (!matchNode) {
109
- db.close();
110
- return {
111
- entry: null,
112
- depth: maxDepth,
113
- steps: [],
114
- leaves: [],
115
- cycles: [],
116
- totalReached: 0,
117
- truncated: false,
128
+ const epType = entryPointType(matchNode.name);
129
+ const entry = {
130
+ name: matchNode.name,
131
+ kind: matchNode.kind,
132
+ file: matchNode.file,
133
+ line: matchNode.line,
134
+ type: epType || 'exported',
135
+ role: matchNode.role,
118
136
  };
119
- }
120
-
121
- const epType = entryPointType(matchNode.name);
122
- const entry = {
123
- name: matchNode.name,
124
- kind: matchNode.kind,
125
- file: matchNode.file,
126
- line: matchNode.line,
127
- type: epType || 'exported',
128
- role: matchNode.role,
129
- };
130
-
131
- // Forward BFS through callees
132
- const visited = new Set([matchNode.id]);
133
- let frontier = [matchNode.id];
134
- const steps = [];
135
- const cycles = [];
136
- let truncated = false;
137
-
138
- // Track which nodes are at each depth and their depth for leaf detection
139
- const nodeDepths = new Map();
140
- const idToNode = new Map();
141
- idToNode.set(matchNode.id, entry);
142
-
143
- for (let d = 1; d <= maxDepth; d++) {
144
- const nextFrontier = [];
145
- const levelNodes = [];
146
-
147
- for (const fid of frontier) {
148
- const callees = db
149
- .prepare(
150
- `SELECT DISTINCT n.id, n.name, n.kind, n.file, n.line, n.role
151
- FROM edges e JOIN nodes n ON e.target_id = n.id
152
- WHERE e.source_id = ? AND e.kind = 'calls'`,
153
- )
154
- .all(fid);
155
-
156
- for (const c of callees) {
157
- if (noTests && isTestFile(c.file)) continue;
158
137
 
159
- if (visited.has(c.id)) {
160
- // Cycle detected
161
- const fromNode = idToNode.get(fid);
162
- if (fromNode) {
163
- cycles.push({ from: fromNode.name, to: c.name, depth: d });
138
+ // Forward BFS through callees
139
+ const visited = new Set([matchNode.id]);
140
+ let frontier = [matchNode.id];
141
+ const steps = [];
142
+ const cycles = [];
143
+ let truncated = false;
144
+
145
+ // Track which nodes are at each depth and their depth for leaf detection
146
+ const nodeDepths = new Map();
147
+ const idToNode = new Map();
148
+ idToNode.set(matchNode.id, entry);
149
+
150
+ for (let d = 1; d <= maxDepth; d++) {
151
+ const nextFrontier = [];
152
+ const levelNodes = [];
153
+
154
+ for (const fid of frontier) {
155
+ const callees = db
156
+ .prepare(
157
+ `SELECT DISTINCT n.id, n.name, n.kind, n.file, n.line, n.role
158
+ FROM edges e JOIN nodes n ON e.target_id = n.id
159
+ WHERE e.source_id = ? AND e.kind = 'calls'`,
160
+ )
161
+ .all(fid);
162
+
163
+ for (const c of callees) {
164
+ if (noTests && isTestFile(c.file)) continue;
165
+
166
+ if (visited.has(c.id)) {
167
+ // Cycle detected
168
+ const fromNode = idToNode.get(fid);
169
+ if (fromNode) {
170
+ cycles.push({ from: fromNode.name, to: c.name, depth: d });
171
+ }
172
+ continue;
164
173
  }
165
- continue;
166
- }
167
174
 
168
- visited.add(c.id);
169
- nextFrontier.push(c.id);
170
- const nodeInfo = { name: c.name, kind: c.kind, file: c.file, line: c.line };
171
- levelNodes.push(nodeInfo);
172
- nodeDepths.set(c.id, d);
173
- idToNode.set(c.id, nodeInfo);
175
+ visited.add(c.id);
176
+ nextFrontier.push(c.id);
177
+ const nodeInfo = { name: c.name, kind: c.kind, file: c.file, line: c.line };
178
+ levelNodes.push(nodeInfo);
179
+ nodeDepths.set(c.id, d);
180
+ idToNode.set(c.id, nodeInfo);
181
+ }
174
182
  }
175
- }
176
-
177
- if (levelNodes.length > 0) {
178
- steps.push({ depth: d, nodes: levelNodes });
179
- }
180
-
181
- frontier = nextFrontier;
182
- if (frontier.length === 0) break;
183
183
 
184
- if (d === maxDepth && frontier.length > 0) {
185
- truncated = true;
186
- }
187
- }
184
+ if (levelNodes.length > 0) {
185
+ steps.push({ depth: d, nodes: levelNodes });
186
+ }
188
187
 
189
- // Identify leaves: visited nodes that have no outgoing 'calls' edges to other visited nodes
190
- // (or no outgoing calls at all)
191
- const leaves = [];
192
- for (const [id, depth] of nodeDepths) {
193
- const outgoing = db
194
- .prepare(
195
- `SELECT DISTINCT n.id
196
- FROM edges e JOIN nodes n ON e.target_id = n.id
197
- WHERE e.source_id = ? AND e.kind = 'calls'`,
198
- )
199
- .all(id);
188
+ frontier = nextFrontier;
189
+ if (frontier.length === 0) break;
200
190
 
201
- if (outgoing.length === 0) {
202
- const node = idToNode.get(id);
203
- if (node) {
204
- leaves.push({ ...node, depth });
191
+ if (d === maxDepth && frontier.length > 0) {
192
+ truncated = true;
205
193
  }
206
194
  }
207
- }
208
-
209
- db.close();
210
- const base = {
211
- entry,
212
- depth: maxDepth,
213
- steps,
214
- leaves,
215
- cycles,
216
- totalReached: visited.size - 1, // exclude the entry node itself
217
- truncated,
218
- };
219
- return paginateResult(base, 'steps', { limit: opts.limit, offset: opts.offset });
220
- }
221
195
 
222
- /**
223
- * Find the best matching node using the same relevance scoring as queries.js findMatchingNodes.
224
- */
225
- function findBestMatch(db, name, opts = {}) {
226
- const kinds = opts.kind
227
- ? [opts.kind]
228
- : [
229
- 'function',
230
- 'method',
231
- 'class',
232
- 'interface',
233
- 'type',
234
- 'struct',
235
- 'enum',
236
- 'trait',
237
- 'record',
238
- 'module',
239
- ];
240
- const placeholders = kinds.map(() => '?').join(', ');
241
- const params = [`%${name}%`, ...kinds];
242
-
243
- let fileCondition = '';
244
- if (opts.file) {
245
- fileCondition = ' AND n.file LIKE ?';
246
- params.push(`%${opts.file}%`);
247
- }
196
+ // Identify leaves: visited nodes that have no outgoing 'calls' edges to other visited nodes
197
+ // (or no outgoing calls at all)
198
+ const leaves = [];
199
+ for (const [id, depth] of nodeDepths) {
200
+ const outgoing = db
201
+ .prepare(
202
+ `SELECT DISTINCT n.id
203
+ FROM edges e JOIN nodes n ON e.target_id = n.id
204
+ WHERE e.source_id = ? AND e.kind = 'calls'`,
205
+ )
206
+ .all(id);
248
207
 
249
- const rows = db
250
- .prepare(
251
- `SELECT n.*, COALESCE(fi.cnt, 0) AS fan_in
252
- FROM nodes n
253
- LEFT JOIN (
254
- SELECT target_id, COUNT(*) AS cnt FROM edges WHERE kind = 'calls' GROUP BY target_id
255
- ) fi ON fi.target_id = n.id
256
- WHERE n.name LIKE ? AND n.kind IN (${placeholders})${fileCondition}`,
257
- )
258
- .all(...params);
259
-
260
- const noTests = opts.noTests || false;
261
- const nodes = noTests ? rows.filter((n) => !isTestFile(n.file)) : rows;
262
-
263
- if (nodes.length === 0) return null;
264
-
265
- const lowerQuery = name.toLowerCase();
266
- for (const node of nodes) {
267
- const lowerName = node.name.toLowerCase();
268
- const bareName = lowerName.includes('.') ? lowerName.split('.').pop() : lowerName;
269
-
270
- let matchScore;
271
- if (lowerName === lowerQuery || bareName === lowerQuery) {
272
- matchScore = 100;
273
- } else if (lowerName.startsWith(lowerQuery) || bareName.startsWith(lowerQuery)) {
274
- matchScore = 60;
275
- } else if (lowerName.includes(`.${lowerQuery}`) || lowerName.includes(`${lowerQuery}.`)) {
276
- matchScore = 40;
277
- } else {
278
- matchScore = 10;
208
+ if (outgoing.length === 0) {
209
+ const node = idToNode.get(id);
210
+ if (node) {
211
+ leaves.push({ ...node, depth });
212
+ }
213
+ }
279
214
  }
280
215
 
281
- const fanInBonus = Math.min(Math.log2(node.fan_in + 1) * 5, 25);
282
- node._relevance = matchScore + fanInBonus;
216
+ const base = {
217
+ entry,
218
+ depth: maxDepth,
219
+ steps,
220
+ leaves,
221
+ cycles,
222
+ totalReached: visited.size - 1, // exclude the entry node itself
223
+ truncated,
224
+ };
225
+ return paginateResult(base, 'steps', { limit: opts.limit, offset: opts.offset });
226
+ } finally {
227
+ db.close();
283
228
  }
284
-
285
- nodes.sort((a, b) => b._relevance - a._relevance);
286
- return nodes[0];
287
229
  }
288
230
 
289
231
  /**
@@ -296,14 +238,7 @@ export function flow(name, dbPath, opts = {}) {
296
238
  limit: opts.limit,
297
239
  offset: opts.offset,
298
240
  });
299
- if (opts.ndjson) {
300
- printNdjson(data, 'entries');
301
- return;
302
- }
303
- if (opts.json) {
304
- console.log(JSON.stringify(data, null, 2));
305
- return;
306
- }
241
+ if (outputResult(data, 'entries', opts)) return;
307
242
  if (data.count === 0) {
308
243
  console.log('No entry points found. Run "codegraph build" first.');
309
244
  return;
@@ -320,10 +255,7 @@ export function flow(name, dbPath, opts = {}) {
320
255
  }
321
256
 
322
257
  const data = flowData(name, dbPath, opts);
323
- if (opts.json) {
324
- console.log(JSON.stringify(data, null, 2));
325
- return;
326
- }
258
+ if (outputResult(data, 'steps', opts)) return;
327
259
 
328
260
  if (!data.entry) {
329
261
  console.log(`No matching entry point or function found for "${name}".`);
package/src/index.js CHANGED
@@ -77,12 +77,24 @@ export {
77
77
  } from './dataflow.js';
78
78
  // Database utilities
79
79
  export {
80
+ countEdges,
81
+ countFiles,
82
+ countNodes,
83
+ fanInJoinSQL,
84
+ fanOutJoinSQL,
80
85
  findDbPath,
86
+ findNodesForTriage,
87
+ findNodesWithFanIn,
81
88
  getBuildMeta,
82
89
  initSchema,
90
+ iterateFunctionNodes,
91
+ kindInClause,
92
+ listFunctionNodes,
93
+ NodeQuery,
83
94
  openDb,
84
95
  openReadonlyOrFail,
85
96
  setBuildMeta,
97
+ testFilterSQL,
86
98
  } from './db.js';
87
99
  // Embeddings
88
100
  export {
@@ -121,7 +133,6 @@ export { isNativeAvailable } from './native.js';
121
133
  export { matchOwners, owners, ownersData, ownersForFiles, parseCodeowners } from './owners.js';
122
134
  // Pagination utilities
123
135
  export { MCP_DEFAULTS, MCP_MAX_LIMIT, paginate, paginateResult, printNdjson } from './paginate.js';
124
-
125
136
  // Unified parser API
126
137
  export { getActiveEngine, isWasmAvailable, parseFileAuto, parseFilesAuto } from './parser.js';
127
138
  // Query functions (data-returning)
@@ -141,7 +152,6 @@ export {
141
152
  FALSE_POSITIVE_CALLER_THRESHOLD,
142
153
  FALSE_POSITIVE_NAMES,
143
154
  fileDepsData,
144
- fileExports,
145
155
  fnDepsData,
146
156
  fnImpactData,
147
157
  impactAnalysisData,
@@ -159,6 +169,24 @@ export {
159
169
  VALID_ROLES,
160
170
  whereData,
161
171
  } from './queries.js';
172
+ // Query CLI display wrappers
173
+ export {
174
+ children,
175
+ context,
176
+ diffImpact,
177
+ explain,
178
+ fileDeps,
179
+ fileExports,
180
+ fnDeps,
181
+ fnImpact,
182
+ impactAnalysis,
183
+ moduleMap,
184
+ queryName,
185
+ roles,
186
+ stats,
187
+ symbolPath,
188
+ where,
189
+ } from './queries-cli.js';
162
190
  // Registry (multi-repo)
163
191
  export {
164
192
  listRepos,
@@ -170,6 +198,10 @@ export {
170
198
  saveRegistry,
171
199
  unregisterRepo,
172
200
  } from './registry.js';
201
+ // Result formatting
202
+ export { outputResult } from './result-formatter.js';
203
+ // Sequence diagram generation
204
+ export { sequence, sequenceData, sequenceToMermaid } from './sequence.js';
173
205
  // Snapshot management
174
206
  export {
175
207
  snapshotDelete,
@@ -191,6 +223,8 @@ export {
191
223
  moduleBoundariesData,
192
224
  structureData,
193
225
  } from './structure.js';
226
+ // Test file detection
227
+ export { isTestFile, TEST_PATTERN } from './test-filter.js';
194
228
  // Triage — composite risk audit
195
229
  export { triage, triageData } from './triage.js';
196
230
  // Interactive HTML viewer
package/src/kinds.js ADDED
@@ -0,0 +1,49 @@
1
+ // ── Symbol kind constants ───────────────────────────────────────────
2
+ // Original 10 kinds — used as default query scope
3
+ export const CORE_SYMBOL_KINDS = [
4
+ 'function',
5
+ 'method',
6
+ 'class',
7
+ 'interface',
8
+ 'type',
9
+ 'struct',
10
+ 'enum',
11
+ 'trait',
12
+ 'record',
13
+ 'module',
14
+ ];
15
+
16
+ // Sub-declaration kinds (Phase 1)
17
+ export const EXTENDED_SYMBOL_KINDS = [
18
+ 'parameter',
19
+ 'property',
20
+ 'constant',
21
+ // Phase 2 (reserved, not yet extracted):
22
+ // 'constructor', 'namespace', 'decorator', 'getter', 'setter',
23
+ ];
24
+
25
+ // Full set for --kind validation and MCP enum
26
+ export const EVERY_SYMBOL_KIND = [...CORE_SYMBOL_KINDS, ...EXTENDED_SYMBOL_KINDS];
27
+
28
+ // Backward compat: ALL_SYMBOL_KINDS stays as the core 10
29
+ export const ALL_SYMBOL_KINDS = CORE_SYMBOL_KINDS;
30
+
31
+ // ── Edge kind constants ─────────────────────────────────────────────
32
+ // Core edge kinds — coupling and dependency relationships
33
+ export const CORE_EDGE_KINDS = [
34
+ 'imports',
35
+ 'imports-type',
36
+ 'reexports',
37
+ 'calls',
38
+ 'extends',
39
+ 'implements',
40
+ 'contains',
41
+ ];
42
+
43
+ // Structural edge kinds — parent/child and type relationships
44
+ export const STRUCTURAL_EDGE_KINDS = ['parameter_of', 'receiver'];
45
+
46
+ // Full set for MCP enum and validation
47
+ export const EVERY_EDGE_KIND = [...CORE_EDGE_KINDS, ...STRUCTURAL_EDGE_KINDS];
48
+
49
+ export const VALID_ROLES = ['entry', 'core', 'utility', 'adapter', 'dead', 'leaf'];
package/src/manifesto.js CHANGED
@@ -3,7 +3,8 @@ import { loadConfig } from './config.js';
3
3
  import { findCycles } from './cycles.js';
4
4
  import { openReadonlyOrFail } from './db.js';
5
5
  import { debug } from './logger.js';
6
- import { paginateResult, printNdjson } from './paginate.js';
6
+ import { paginateResult } from './paginate.js';
7
+ import { outputResult } from './result-formatter.js';
7
8
 
8
9
  // ─── Rule Definitions ─────────────────────────────────────────────────
9
10
 
@@ -434,13 +435,7 @@ export function manifestoData(customDbPath, opts = {}) {
434
435
  export function manifesto(customDbPath, opts = {}) {
435
436
  const data = manifestoData(customDbPath, opts);
436
437
 
437
- if (opts.ndjson) {
438
- printNdjson(data, 'violations');
439
- if (!data.passed) process.exit(1);
440
- return;
441
- }
442
- if (opts.json) {
443
- console.log(JSON.stringify(data, null, 2));
438
+ if (outputResult(data, 'violations', opts)) {
444
439
  if (!data.passed) process.exit(1);
445
440
  return;
446
441
  }