@kodus/kodus-graph 0.2.7 → 0.2.8

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kodus/kodus-graph",
3
- "version": "0.2.7",
3
+ "version": "0.2.8",
4
4
  "description": "Code graph builder for Kodus code review — parses source code into structural graphs with nodes, edges, and analysis",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,5 +1,6 @@
1
1
  import type { IndexedGraph } from '../graph/loader';
2
2
  import type { GraphEdge, GraphNode } from '../graph/types';
3
+ import { log } from '../shared/logger';
3
4
 
4
5
  export interface NodeChange {
5
6
  qualified_name: string;
@@ -45,6 +46,12 @@ export function computeStructuralDiff(
45
46
  if (changedSet.has(n.file_path)) newNodesMap.set(n.qualified_name, n);
46
47
  }
47
48
 
49
+ log.debug('diff: input', {
50
+ oldNodesInChanged: oldNodesInChanged.size,
51
+ newNodesInChanged: newNodesMap.size,
52
+ changedFiles,
53
+ });
54
+
48
55
  // Classify nodes
49
56
  const added: NodeChange[] = [];
50
57
  const removed: NodeChange[] = [];
@@ -79,12 +86,34 @@ export function computeStructuralDiff(
79
86
  if (n.content_hash && newN.content_hash) {
80
87
  // Definitive: hash comparison catches ALL content changes,
81
88
  // even same-line-count edits (e.g. `return 1` → `return 2`).
82
- if (n.content_hash !== newN.content_hash) changes.push('body');
89
+ if (n.content_hash !== newN.content_hash) {
90
+ changes.push('body');
91
+ log.debug('diff: body change detected', {
92
+ node: qn,
93
+ oldHash: n.content_hash.substring(0, 8),
94
+ newHash: newN.content_hash.substring(0, 8),
95
+ });
96
+ } else {
97
+ log.debug('diff: hash match (displacement only)', {
98
+ node: qn,
99
+ oldLines: `${n.line_start}-${n.line_end}`,
100
+ newLines: `${newN.line_start}-${newN.line_end}`,
101
+ });
102
+ }
83
103
  } else if (n.line_start !== newN.line_start || n.line_end !== newN.line_end) {
84
104
  // Fallback (legacy data without content_hash): size heuristic.
85
105
  const oldSize = n.line_end - n.line_start;
86
106
  const newSize = newN.line_end - newN.line_start;
87
- if (oldSize !== newSize) changes.push('line_range');
107
+ if (oldSize !== newSize) {
108
+ changes.push('line_range');
109
+ log.debug('diff: line_range fallback (no content_hash)', {
110
+ node: qn,
111
+ hasOldHash: !!n.content_hash,
112
+ hasNewHash: !!newN.content_hash,
113
+ oldSize,
114
+ newSize,
115
+ });
116
+ }
88
117
  }
89
118
  if ((n.params || '') !== (newN.params || '')) changes.push('params');
90
119
  if ((n.return_type || '') !== (newN.return_type || '')) changes.push('return_type');
@@ -121,6 +150,15 @@ export function computeStructuralDiff(
121
150
  riskByFile[file] = { dependents: count, risk };
122
151
  }
123
152
 
153
+ log.info('diff: result', {
154
+ added: added.length,
155
+ removed: removed.length,
156
+ modified: modified.length,
157
+ edgesAdded: addedEdges.length,
158
+ edgesRemoved: removedEdges.length,
159
+ modifiedDetails: modified.map((m) => `${m.qualified_name} [${m.changes.join(',')}]`),
160
+ });
161
+
124
162
  return {
125
163
  changed_files: changedFiles,
126
164
  summary: { added: added.length, removed: removed.length, modified: modified.length },
package/src/cli.ts CHANGED
@@ -14,6 +14,9 @@ import { executeUpdate } from './commands/update';
14
14
  const program = new Command();
15
15
 
16
16
  import pkg from '../package.json';
17
+ import { log } from './shared/logger';
18
+
19
+ log.info(`kodus-graph v${pkg.version}`, { node: process.version, platform: process.platform });
17
20
  program.name('kodus-graph').description('Code graph builder for Kodus code review').version(pkg.version);
18
21
 
19
22
  program
@@ -22,6 +22,15 @@ interface ContextOptions {
22
22
  export async function executeContext(opts: ContextOptions): Promise<void> {
23
23
  const repoDir = resolve(opts.repoDir);
24
24
 
25
+ log.info('context: starting', {
26
+ files: opts.files,
27
+ repoDir,
28
+ graph: opts.graph ?? null,
29
+ format: opts.format,
30
+ minConfidence: opts.minConfidence,
31
+ maxDepth: opts.maxDepth,
32
+ });
33
+
25
34
  // Parse changed files using secure temp
26
35
  const tmp = createSecureTempFile('ctx');
27
36
  try {
@@ -33,6 +42,11 @@ export async function executeContext(opts: ContextOptions): Promise<void> {
33
42
  });
34
43
  const parseResult = JSON.parse(readFileSync(tmp.filePath, 'utf-8'));
35
44
 
45
+ log.info('context: parse done', {
46
+ nodes: parseResult.nodes?.length ?? 0,
47
+ edges: parseResult.edges?.length ?? 0,
48
+ });
49
+
36
50
  // Load and merge with main graph if provided
37
51
  let mergedGraph: GraphData;
38
52
  let oldGraph: GraphData | null = null;
@@ -53,6 +67,12 @@ export async function executeContext(opts: ContextOptions): Promise<void> {
53
67
  const changedSet = new Set(opts.files);
54
68
  const sameBranch = detectSameBranch(validated.data.nodes, parseResult.nodes, changedSet);
55
69
 
70
+ log.info('context: baseline graph loaded', {
71
+ graphNodes: validated.data.nodes.length,
72
+ graphEdges: validated.data.edges.length,
73
+ sameBranch,
74
+ });
75
+
56
76
  if (sameBranch) {
57
77
  // --graph was built from the same commit (e.g. kodus-ai's parse --all on PR branch).
58
78
  // Exclude changed files from oldGraph so diff detects their functions as "added"
@@ -88,6 +108,16 @@ export async function executeContext(opts: ContextOptions): Promise<void> {
88
108
  maxDepth: opts.maxDepth,
89
109
  });
90
110
 
111
+ log.info('context: analysis done', {
112
+ changedFunctions: output.analysis.changed_functions.length,
113
+ diff: output.analysis.structural_diff.summary,
114
+ blastRadius: output.analysis.blast_radius.total_functions,
115
+ risk: `${output.analysis.risk.level} (${output.analysis.risk.score})`,
116
+ testGaps: output.analysis.test_gaps.length,
117
+ affectedFlows: output.analysis.affected_flows.length,
118
+ duration_ms: output.analysis.metadata.duration_ms,
119
+ });
120
+
91
121
  if (opts.format === 'prompt') {
92
122
  writeFileSync(opts.out, formatPrompt(output));
93
123
  } else {
@@ -120,7 +150,10 @@ function detectSameBranch(
120
150
  }
121
151
 
122
152
  // No overlap means graph has no nodes for changed files — not same-branch scenario
123
- if (graphHashes.size === 0) return false;
153
+ if (graphHashes.size === 0) {
154
+ log.debug('detectSameBranch: no graph hashes for changed files');
155
+ return false;
156
+ }
124
157
 
125
158
  const parseHashes = new Map<string, string>();
126
159
  for (const n of parseNodes) {
@@ -132,8 +165,18 @@ function detectSameBranch(
132
165
  // If any overlapping file has different hash → different branch
133
166
  for (const [file, hash] of graphHashes) {
134
167
  const parseHash = parseHashes.get(file);
135
- if (parseHash && parseHash !== hash) return false;
168
+ if (parseHash && parseHash !== hash) {
169
+ log.debug('detectSameBranch: hash mismatch → different branch', {
170
+ file,
171
+ graphHash: hash.substring(0, 8),
172
+ parseHash: parseHash.substring(0, 8),
173
+ });
174
+ return false;
175
+ }
136
176
  }
137
177
 
178
+ log.debug('detectSameBranch: all hashes match → same branch', {
179
+ filesCompared: graphHashes.size,
180
+ });
138
181
  return true;
139
182
  }
@@ -1,5 +1,8 @@
1
1
  // src/shared/logger.ts
2
2
  export const log = {
3
+ info(msg: string, ctx?: Record<string, unknown>): void {
4
+ process.stderr.write(`[INFO] ${msg}${ctx ? ` ${JSON.stringify(ctx)}` : ''}\n`);
5
+ },
3
6
  debug(msg: string, ctx?: Record<string, unknown>): void {
4
7
  if (process.env.KODUS_GRAPH_DEBUG) {
5
8
  process.stderr.write(`[DEBUG] ${msg}${ctx ? ` ${JSON.stringify(ctx)}` : ''}\n`);