@optave/codegraph 2.5.1 → 3.0.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.
@@ -31,11 +31,13 @@ export function extractRubySymbols(tree, _filePath) {
31
31
  case 'class': {
32
32
  const nameNode = node.childForFieldName('name');
33
33
  if (nameNode) {
34
+ const classChildren = extractRubyClassChildren(node);
34
35
  definitions.push({
35
36
  name: nameNode.text,
36
37
  kind: 'class',
37
38
  line: node.startPosition.row + 1,
38
39
  endLine: nodeEndLine(node),
40
+ children: classChildren.length > 0 ? classChildren : undefined,
39
41
  });
40
42
  const superclass = node.childForFieldName('superclass');
41
43
  if (superclass) {
@@ -73,11 +75,13 @@ export function extractRubySymbols(tree, _filePath) {
73
75
  case 'module': {
74
76
  const nameNode = node.childForFieldName('name');
75
77
  if (nameNode) {
78
+ const moduleChildren = extractRubyBodyConstants(node);
76
79
  definitions.push({
77
80
  name: nameNode.text,
78
81
  kind: 'module',
79
82
  line: node.startPosition.row + 1,
80
83
  endLine: nodeEndLine(node),
84
+ children: moduleChildren.length > 0 ? moduleChildren : undefined,
81
85
  });
82
86
  }
83
87
  break;
@@ -88,11 +92,13 @@ export function extractRubySymbols(tree, _filePath) {
88
92
  if (nameNode) {
89
93
  const parentClass = findRubyParentClass(node);
90
94
  const fullName = parentClass ? `${parentClass}.${nameNode.text}` : nameNode.text;
95
+ const params = extractRubyParameters(node);
91
96
  definitions.push({
92
97
  name: fullName,
93
98
  kind: 'method',
94
99
  line: node.startPosition.row + 1,
95
100
  endLine: nodeEndLine(node),
101
+ children: params.length > 0 ? params : undefined,
96
102
  });
97
103
  }
98
104
  break;
@@ -103,16 +109,34 @@ export function extractRubySymbols(tree, _filePath) {
103
109
  if (nameNode) {
104
110
  const parentClass = findRubyParentClass(node);
105
111
  const fullName = parentClass ? `${parentClass}.${nameNode.text}` : nameNode.text;
112
+ const params = extractRubyParameters(node);
106
113
  definitions.push({
107
114
  name: fullName,
108
115
  kind: 'function',
109
116
  line: node.startPosition.row + 1,
110
117
  endLine: nodeEndLine(node),
118
+ children: params.length > 0 ? params : undefined,
111
119
  });
112
120
  }
113
121
  break;
114
122
  }
115
123
 
124
+ case 'assignment': {
125
+ // Top-level constant assignments (parent is program)
126
+ if (node.parent && node.parent.type === 'program') {
127
+ const left = node.childForFieldName('left');
128
+ if (left && left.type === 'constant') {
129
+ definitions.push({
130
+ name: left.text,
131
+ kind: 'constant',
132
+ line: node.startPosition.row + 1,
133
+ endLine: nodeEndLine(node),
134
+ });
135
+ }
136
+ }
137
+ break;
138
+ }
139
+
116
140
  case 'call': {
117
141
  const methodNode = node.childForFieldName('method');
118
142
  if (methodNode) {
@@ -186,3 +210,68 @@ export function extractRubySymbols(tree, _filePath) {
186
210
  walkRubyNode(tree.rootNode);
187
211
  return { definitions, calls, imports, classes, exports };
188
212
  }
213
+
214
+ // ── Child extraction helpers ────────────────────────────────────────────────
215
+
216
+ const RUBY_PARAM_TYPES = new Set([
217
+ 'identifier',
218
+ 'optional_parameter',
219
+ 'splat_parameter',
220
+ 'hash_splat_parameter',
221
+ 'block_parameter',
222
+ 'keyword_parameter',
223
+ ]);
224
+
225
+ function extractRubyParameters(methodNode) {
226
+ const params = [];
227
+ const paramList =
228
+ methodNode.childForFieldName('parameters') || findChild(methodNode, 'method_parameters');
229
+ if (!paramList) return params;
230
+ for (let i = 0; i < paramList.childCount; i++) {
231
+ const param = paramList.child(i);
232
+ if (!param || !RUBY_PARAM_TYPES.has(param.type)) continue;
233
+ let name;
234
+ if (param.type === 'identifier') {
235
+ name = param.text;
236
+ } else {
237
+ // Compound parameter types have an identifier child for the name
238
+ const id = findChild(param, 'identifier');
239
+ name = id ? id.text : param.text;
240
+ }
241
+ params.push({ name, kind: 'parameter', line: param.startPosition.row + 1 });
242
+ }
243
+ return params;
244
+ }
245
+
246
+ function extractRubyBodyConstants(containerNode) {
247
+ const children = [];
248
+ const body = containerNode.childForFieldName('body') || findChild(containerNode, 'body');
249
+ if (!body) return children;
250
+ for (let i = 0; i < body.childCount; i++) {
251
+ const child = body.child(i);
252
+ if (!child || child.type !== 'assignment') continue;
253
+ const left = child.childForFieldName('left');
254
+ if (left && left.type === 'constant') {
255
+ children.push({ name: left.text, kind: 'constant', line: child.startPosition.row + 1 });
256
+ }
257
+ }
258
+ return children;
259
+ }
260
+
261
+ function extractRubyClassChildren(classNode) {
262
+ const children = [];
263
+ const body = classNode.childForFieldName('body') || findChild(classNode, 'body');
264
+ if (!body) return children;
265
+ for (let i = 0; i < body.childCount; i++) {
266
+ const child = body.child(i);
267
+ if (!child || child.type !== 'assignment') continue;
268
+ const left = child.childForFieldName('left');
269
+ if (!left) continue;
270
+ if (left.type === 'instance_variable') {
271
+ children.push({ name: left.text, kind: 'property', line: child.startPosition.row + 1 });
272
+ } else if (left.type === 'constant') {
273
+ children.push({ name: left.text, kind: 'constant', line: child.startPosition.row + 1 });
274
+ }
275
+ }
276
+ return children;
277
+ }
@@ -1,4 +1,4 @@
1
- import { nodeEndLine } from './helpers.js';
1
+ import { findChild, nodeEndLine } from './helpers.js';
2
2
 
3
3
  /**
4
4
  * Extract symbols from Rust files.
@@ -30,11 +30,13 @@ export function extractRustSymbols(tree, _filePath) {
30
30
  const implType = findCurrentImpl(node);
31
31
  const fullName = implType ? `${implType}.${nameNode.text}` : nameNode.text;
32
32
  const kind = implType ? 'method' : 'function';
33
+ const params = extractRustParameters(node.childForFieldName('parameters'));
33
34
  definitions.push({
34
35
  name: fullName,
35
36
  kind,
36
37
  line: node.startPosition.row + 1,
37
38
  endLine: nodeEndLine(node),
39
+ children: params.length > 0 ? params : undefined,
38
40
  });
39
41
  }
40
42
  break;
@@ -43,11 +45,13 @@ export function extractRustSymbols(tree, _filePath) {
43
45
  case 'struct_item': {
44
46
  const nameNode = node.childForFieldName('name');
45
47
  if (nameNode) {
48
+ const fields = extractStructFields(node);
46
49
  definitions.push({
47
50
  name: nameNode.text,
48
51
  kind: 'struct',
49
52
  line: node.startPosition.row + 1,
50
53
  endLine: nodeEndLine(node),
54
+ children: fields.length > 0 ? fields : undefined,
51
55
  });
52
56
  }
53
57
  break;
@@ -56,11 +60,26 @@ export function extractRustSymbols(tree, _filePath) {
56
60
  case 'enum_item': {
57
61
  const nameNode = node.childForFieldName('name');
58
62
  if (nameNode) {
63
+ const variants = extractEnumVariants(node);
59
64
  definitions.push({
60
65
  name: nameNode.text,
61
66
  kind: 'enum',
62
67
  line: node.startPosition.row + 1,
63
68
  endLine: nodeEndLine(node),
69
+ children: variants.length > 0 ? variants : undefined,
70
+ });
71
+ }
72
+ break;
73
+ }
74
+
75
+ case 'const_item': {
76
+ const nameNode = node.childForFieldName('name');
77
+ if (nameNode) {
78
+ definitions.push({
79
+ name: nameNode.text,
80
+ kind: 'constant',
81
+ line: node.startPosition.row + 1,
82
+ endLine: nodeEndLine(node),
64
83
  });
65
84
  }
66
85
  break;
@@ -170,6 +189,57 @@ export function extractRustSymbols(tree, _filePath) {
170
189
  return { definitions, calls, imports, classes, exports };
171
190
  }
172
191
 
192
+ // ── Child extraction helpers ────────────────────────────────────────────────
193
+
194
+ function extractRustParameters(paramListNode) {
195
+ const params = [];
196
+ if (!paramListNode) return params;
197
+ for (let i = 0; i < paramListNode.childCount; i++) {
198
+ const param = paramListNode.child(i);
199
+ if (!param) continue;
200
+ if (param.type === 'self_parameter') {
201
+ params.push({ name: 'self', kind: 'parameter', line: param.startPosition.row + 1 });
202
+ } else if (param.type === 'parameter') {
203
+ const pattern = param.childForFieldName('pattern');
204
+ if (pattern) {
205
+ params.push({ name: pattern.text, kind: 'parameter', line: param.startPosition.row + 1 });
206
+ }
207
+ }
208
+ }
209
+ return params;
210
+ }
211
+
212
+ function extractStructFields(structNode) {
213
+ const fields = [];
214
+ const fieldList =
215
+ structNode.childForFieldName('body') || findChild(structNode, 'field_declaration_list');
216
+ if (!fieldList) return fields;
217
+ for (let i = 0; i < fieldList.childCount; i++) {
218
+ const field = fieldList.child(i);
219
+ if (!field || field.type !== 'field_declaration') continue;
220
+ const nameNode = field.childForFieldName('name');
221
+ if (nameNode) {
222
+ fields.push({ name: nameNode.text, kind: 'property', line: field.startPosition.row + 1 });
223
+ }
224
+ }
225
+ return fields;
226
+ }
227
+
228
+ function extractEnumVariants(enumNode) {
229
+ const variants = [];
230
+ const body = enumNode.childForFieldName('body') || findChild(enumNode, 'enum_variant_list');
231
+ if (!body) return variants;
232
+ for (let i = 0; i < body.childCount; i++) {
233
+ const variant = body.child(i);
234
+ if (!variant || variant.type !== 'enum_variant') continue;
235
+ const nameNode = variant.childForFieldName('name');
236
+ if (nameNode) {
237
+ variants.push({ name: nameNode.text, kind: 'constant', line: variant.startPosition.row + 1 });
238
+ }
239
+ }
240
+ return variants;
241
+ }
242
+
173
243
  function extractRustUsePath(node) {
174
244
  if (!node) return [];
175
245
 
package/src/flow.js CHANGED
@@ -6,7 +6,7 @@
6
6
  */
7
7
 
8
8
  import { openReadonlyOrFail } from './db.js';
9
- import { paginateResult } from './paginate.js';
9
+ import { paginateResult, printNdjson } from './paginate.js';
10
10
  import { isTestFile, kindIcon } from './queries.js';
11
11
  import { FRAMEWORK_ENTRY_PREFIXES } from './structure.js';
12
12
 
@@ -204,7 +204,7 @@ export function flowData(name, dbPath, opts = {}) {
204
204
  }
205
205
 
206
206
  db.close();
207
- return {
207
+ const base = {
208
208
  entry,
209
209
  depth: maxDepth,
210
210
  steps,
@@ -213,6 +213,7 @@ export function flowData(name, dbPath, opts = {}) {
213
213
  totalReached: visited.size - 1, // exclude the entry node itself
214
214
  truncated,
215
215
  };
216
+ return paginateResult(base, 'steps', { limit: opts.limit, offset: opts.offset });
216
217
  }
217
218
 
218
219
  /**
@@ -293,8 +294,7 @@ export function flow(name, dbPath, opts = {}) {
293
294
  offset: opts.offset,
294
295
  });
295
296
  if (opts.ndjson) {
296
- if (data._pagination) console.log(JSON.stringify({ _meta: data._pagination }));
297
- for (const e of data.entries) console.log(JSON.stringify(e));
297
+ printNdjson(data, 'entries');
298
298
  return;
299
299
  }
300
300
  if (opts.json) {
package/src/index.js CHANGED
@@ -5,10 +5,37 @@
5
5
  * import { buildGraph, queryNameData, findCycles, exportDOT } from 'codegraph';
6
6
  */
7
7
 
8
+ // AST node queries
9
+ export { AST_NODE_KINDS, astQuery, astQueryData } from './ast.js';
10
+ // Audit (composite report)
11
+ export { audit, auditData } from './audit.js';
12
+ // Batch querying
13
+ export {
14
+ BATCH_COMMANDS,
15
+ batch,
16
+ batchData,
17
+ batchQuery,
18
+ multiBatchData,
19
+ splitTargets,
20
+ } from './batch.js';
21
+ // Architecture boundary rules
22
+ export { evaluateBoundaries, PRESETS, validateBoundaryConfig } from './boundaries.js';
8
23
  // Branch comparison
9
24
  export { branchCompareData, branchCompareMermaid } from './branch-compare.js';
10
25
  // Graph building
11
26
  export { buildGraph, collectFiles, loadPathAliases, resolveImportPath } from './builder.js';
27
+ // Control flow graph (intraprocedural)
28
+ export {
29
+ buildCFGData,
30
+ buildFunctionCFG,
31
+ CFG_RULES,
32
+ cfg,
33
+ cfgData,
34
+ cfgToDOT,
35
+ cfgToMermaid,
36
+ } from './cfg.js';
37
+ // Check (CI validation predicates)
38
+ export { check, checkData } from './check.js';
12
39
  // Co-change analysis
13
40
  export {
14
41
  analyzeCoChanges,
@@ -29,7 +56,9 @@ export {
29
56
  computeHalsteadMetrics,
30
57
  computeLOCMetrics,
31
58
  computeMaintainabilityIndex,
59
+ findFunctionNode,
32
60
  HALSTEAD_RULES,
61
+ iterComplexity,
33
62
  } from './complexity.js';
34
63
  // Configuration
35
64
  export { loadConfig } from './config.js';
@@ -37,6 +66,15 @@ export { loadConfig } from './config.js';
37
66
  export { EXTENSIONS, IGNORE_DIRS, normalizePath } from './constants.js';
38
67
  // Circular dependency detection
39
68
  export { findCycles, formatCycles } from './cycles.js';
69
+ // Dataflow analysis
70
+ export {
71
+ buildDataflowEdges,
72
+ dataflow,
73
+ dataflowData,
74
+ dataflowImpactData,
75
+ dataflowPathData,
76
+ extractDataflow,
77
+ } from './dataflow.js';
40
78
  // Database utilities
41
79
  export {
42
80
  findDbPath,
@@ -55,13 +93,22 @@ export {
55
93
  EMBEDDING_STRATEGIES,
56
94
  embed,
57
95
  estimateTokens,
96
+ ftsSearchData,
97
+ hybridSearchData,
58
98
  MODELS,
59
99
  multiSearchData,
60
100
  search,
61
101
  searchData,
62
102
  } from './embedder.js';
63
- // Export (DOT/Mermaid/JSON)
64
- export { exportDOT, exportJSON, exportMermaid } from './export.js';
103
+ // Export (DOT/Mermaid/JSON/GraphML/GraphSON/Neo4j CSV)
104
+ export {
105
+ exportDOT,
106
+ exportGraphML,
107
+ exportGraphSON,
108
+ exportJSON,
109
+ exportMermaid,
110
+ exportNeo4jCSV,
111
+ } from './export.js';
65
112
  // Execution flow tracing
66
113
  export { entryPointType, flowData, listEntryPointsData } from './flow.js';
67
114
  // Logger
@@ -70,29 +117,44 @@ export { setVerbose } from './logger.js';
70
117
  export { manifesto, manifestoData, RULE_DEFS } from './manifesto.js';
71
118
  // Native engine
72
119
  export { isNativeAvailable } from './native.js';
120
+ // Ownership (CODEOWNERS)
121
+ export { matchOwners, owners, ownersData, ownersForFiles, parseCodeowners } from './owners.js';
73
122
  // Pagination utilities
74
- export { MCP_DEFAULTS, MCP_MAX_LIMIT, paginate, paginateResult } from './paginate.js';
123
+ export { MCP_DEFAULTS, MCP_MAX_LIMIT, paginate, paginateResult, printNdjson } from './paginate.js';
75
124
 
76
125
  // Unified parser API
77
126
  export { getActiveEngine, parseFileAuto, parseFilesAuto } from './parser.js';
78
127
  // Query functions (data-returning)
79
128
  export {
80
129
  ALL_SYMBOL_KINDS,
130
+ CORE_EDGE_KINDS,
131
+ CORE_SYMBOL_KINDS,
132
+ childrenData,
81
133
  contextData,
82
134
  diffImpactData,
83
135
  diffImpactMermaid,
136
+ EVERY_EDGE_KIND,
137
+ EVERY_SYMBOL_KIND,
138
+ EXTENDED_SYMBOL_KINDS,
84
139
  explainData,
140
+ exportsData,
85
141
  FALSE_POSITIVE_CALLER_THRESHOLD,
86
142
  FALSE_POSITIVE_NAMES,
87
143
  fileDepsData,
144
+ fileExports,
88
145
  fnDepsData,
89
146
  fnImpactData,
90
147
  impactAnalysisData,
148
+ iterListFunctions,
149
+ iterRoles,
150
+ iterWhere,
91
151
  kindIcon,
92
152
  moduleMapData,
153
+ normalizeSymbol,
93
154
  pathData,
94
155
  queryNameData,
95
156
  rolesData,
157
+ STRUCTURAL_EDGE_KINDS,
96
158
  statsData,
97
159
  VALID_ROLES,
98
160
  whereData,
@@ -108,6 +170,15 @@ export {
108
170
  saveRegistry,
109
171
  unregisterRepo,
110
172
  } from './registry.js';
173
+ // Snapshot management
174
+ export {
175
+ snapshotDelete,
176
+ snapshotList,
177
+ snapshotRestore,
178
+ snapshotSave,
179
+ snapshotsDir,
180
+ validateSnapshotName,
181
+ } from './snapshot.js';
111
182
  // Structure analysis
112
183
  export {
113
184
  buildStructure,
@@ -120,5 +191,9 @@ export {
120
191
  moduleBoundariesData,
121
192
  structureData,
122
193
  } from './structure.js';
194
+ // Triage — composite risk audit
195
+ export { triage, triageData } from './triage.js';
196
+ // Interactive HTML viewer
197
+ export { generatePlotHTML, loadPlotConfig } from './viewer.js';
123
198
  // Watch mode
124
199
  export { watchProject } from './watcher.js';
package/src/manifesto.js CHANGED
@@ -1,7 +1,9 @@
1
+ import { evaluateBoundaries } from './boundaries.js';
1
2
  import { loadConfig } from './config.js';
2
3
  import { findCycles } from './cycles.js';
3
4
  import { openReadonlyOrFail } from './db.js';
4
5
  import { debug } from './logger.js';
6
+ import { paginateResult, printNdjson } from './paginate.js';
5
7
 
6
8
  // ─── Rule Definitions ─────────────────────────────────────────────────
7
9
 
@@ -54,6 +56,12 @@ export const RULE_DEFS = [
54
56
  { name: 'fanIn', level: 'file', metric: 'fan_in', defaults: { warn: null, fail: null } },
55
57
  { name: 'fanOut', level: 'file', metric: 'fan_out', defaults: { warn: null, fail: null } },
56
58
  { name: 'noCycles', level: 'graph', metric: 'noCycles', defaults: { warn: null, fail: null } },
59
+ {
60
+ name: 'boundaries',
61
+ level: 'graph',
62
+ metric: 'boundaries',
63
+ defaults: { warn: null, fail: null },
64
+ },
57
65
  ];
58
66
 
59
67
  // ─── Helpers ──────────────────────────────────────────────────────────
@@ -318,6 +326,59 @@ function evaluateGraphRules(db, rules, opts, violations, ruleResults) {
318
326
  });
319
327
  }
320
328
 
329
+ function evaluateBoundaryRules(db, rules, config, opts, violations, ruleResults) {
330
+ const thresholds = rules.boundaries;
331
+ const boundaryConfig = config.manifesto?.boundaries;
332
+
333
+ // Auto-enable at warn level when boundary config exists but threshold not set
334
+ const effectiveThresholds = { ...thresholds };
335
+ if (boundaryConfig && !isEnabled(thresholds)) {
336
+ effectiveThresholds.warn = true;
337
+ }
338
+
339
+ if (!isEnabled(effectiveThresholds) || !boundaryConfig) {
340
+ ruleResults.push({
341
+ name: 'boundaries',
342
+ level: 'graph',
343
+ status: 'pass',
344
+ thresholds: effectiveThresholds,
345
+ violationCount: 0,
346
+ });
347
+ return;
348
+ }
349
+
350
+ const result = evaluateBoundaries(db, boundaryConfig, { noTests: opts.noTests || false });
351
+ const hasBoundaryViolations = result.violationCount > 0;
352
+
353
+ if (!hasBoundaryViolations) {
354
+ ruleResults.push({
355
+ name: 'boundaries',
356
+ level: 'graph',
357
+ status: 'pass',
358
+ thresholds: effectiveThresholds,
359
+ violationCount: 0,
360
+ });
361
+ return;
362
+ }
363
+
364
+ const level = effectiveThresholds.fail != null ? 'fail' : 'warn';
365
+
366
+ for (const v of result.violations) {
367
+ violations.push({
368
+ ...v,
369
+ level,
370
+ });
371
+ }
372
+
373
+ ruleResults.push({
374
+ name: 'boundaries',
375
+ level: 'graph',
376
+ status: level,
377
+ thresholds: effectiveThresholds,
378
+ violationCount: result.violationCount,
379
+ });
380
+ }
381
+
321
382
  // ─── Public API ───────────────────────────────────────────────────────
322
383
 
323
384
  /**
@@ -343,6 +404,7 @@ export function manifestoData(customDbPath, opts = {}) {
343
404
  evaluateFunctionRules(db, rules, opts, violations, ruleResults);
344
405
  evaluateFileRules(db, rules, opts, violations, ruleResults);
345
406
  evaluateGraphRules(db, rules, opts, violations, ruleResults);
407
+ evaluateBoundaryRules(db, rules, config, opts, violations, ruleResults);
346
408
 
347
409
  const failViolations = violations.filter((v) => v.level === 'fail');
348
410
 
@@ -354,12 +416,13 @@ export function manifestoData(customDbPath, opts = {}) {
354
416
  violationCount: violations.length,
355
417
  };
356
418
 
357
- return {
419
+ const base = {
358
420
  rules: ruleResults,
359
421
  violations,
360
422
  summary,
361
423
  passed: failViolations.length === 0,
362
424
  };
425
+ return paginateResult(base, 'violations', { limit: opts.limit, offset: opts.offset });
363
426
  } finally {
364
427
  db.close();
365
428
  }
@@ -371,6 +434,11 @@ export function manifestoData(customDbPath, opts = {}) {
371
434
  export function manifesto(customDbPath, opts = {}) {
372
435
  const data = manifestoData(customDbPath, opts);
373
436
 
437
+ if (opts.ndjson) {
438
+ printNdjson(data, 'violations');
439
+ if (!data.passed) process.exit(1);
440
+ return;
441
+ }
374
442
  if (opts.json) {
375
443
  console.log(JSON.stringify(data, null, 2));
376
444
  if (!data.passed) process.exit(1);