codesight 1.3.1 → 1.4.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.
package/dist/eval.js ADDED
@@ -0,0 +1,184 @@
1
+ /**
2
+ * Evaluation suite: runs codesight on fixture repos and measures
3
+ * precision, recall, and F1 against ground truth.
4
+ */
5
+ import { readFile, writeFile, mkdir, rm } from "node:fs/promises";
6
+ import { join, dirname } from "node:path";
7
+ import { fileURLToPath } from "node:url";
8
+ import { collectFiles, detectProject } from "./scanner.js";
9
+ import { detectRoutes } from "./detectors/routes.js";
10
+ import { detectSchemas } from "./detectors/schema.js";
11
+ import { detectComponents } from "./detectors/components.js";
12
+ import { detectConfig } from "./detectors/config.js";
13
+ import { detectMiddleware } from "./detectors/middleware.js";
14
+ function calcMetrics(detected, expected) {
15
+ let tp = 0;
16
+ let fp = 0;
17
+ let fn = 0;
18
+ for (const item of detected) {
19
+ if (expected.has(item))
20
+ tp++;
21
+ else
22
+ fp++;
23
+ }
24
+ for (const item of expected) {
25
+ if (!detected.has(item))
26
+ fn++;
27
+ }
28
+ const precision = tp + fp > 0 ? tp / (tp + fp) : 1;
29
+ const recall = tp + fn > 0 ? tp / (tp + fn) : 1;
30
+ const f1 = precision + recall > 0 ? (2 * precision * recall) / (precision + recall) : 0;
31
+ return {
32
+ precision: Math.round(precision * 1000) / 1000,
33
+ recall: Math.round(recall * 1000) / 1000,
34
+ f1: Math.round(f1 * 1000) / 1000,
35
+ truePositives: tp,
36
+ falsePositives: fp,
37
+ falseNegatives: fn,
38
+ };
39
+ }
40
+ async function createTempRepo(fixture) {
41
+ const tmpDir = join((await import("node:os")).tmpdir(), `codesight-eval-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`);
42
+ for (const [filePath, content] of Object.entries(fixture.files)) {
43
+ const fullPath = join(tmpDir, filePath);
44
+ await mkdir(dirname(fullPath), { recursive: true });
45
+ await writeFile(fullPath, content);
46
+ }
47
+ return tmpDir;
48
+ }
49
+ async function evalFixture(fixturePath) {
50
+ const repoJson = JSON.parse(await readFile(join(fixturePath, "repo.json"), "utf-8"));
51
+ const groundTruth = JSON.parse(await readFile(join(fixturePath, "ground-truth.json"), "utf-8"));
52
+ // Create temp repo from fixture
53
+ const tmpDir = await createTempRepo(repoJson);
54
+ const startTime = Date.now();
55
+ try {
56
+ // Run codesight detectors
57
+ const project = await detectProject(tmpDir);
58
+ const files = await collectFiles(tmpDir, 10);
59
+ const [routes, schemas, components, config, middleware] = await Promise.all([
60
+ detectRoutes(files, project),
61
+ detectSchemas(files, project),
62
+ detectComponents(files, project),
63
+ detectConfig(files, project),
64
+ detectMiddleware(files, project),
65
+ ]);
66
+ const runtime = Date.now() - startTime;
67
+ // Compare routes: method:path
68
+ const detectedRoutes = new Set(routes.map((r) => `${r.method}:${r.path}`));
69
+ const expectedRoutes = new Set((groundTruth.routes || []).map((r) => `${r.method}:${r.path}`));
70
+ // Compare models: name
71
+ const detectedModels = new Set(schemas.map((s) => s.name.toLowerCase()));
72
+ const expectedModels = new Set((groundTruth.models || []).map((m) => m.name.toLowerCase()));
73
+ // Compare env vars
74
+ const detectedEnvVars = new Set(config.envVars.map((e) => e.name));
75
+ const expectedEnvVars = new Set(groundTruth.envVars || []);
76
+ const result = {
77
+ name: repoJson.name,
78
+ routes: calcMetrics(detectedRoutes, expectedRoutes),
79
+ models: calcMetrics(detectedModels, expectedModels),
80
+ envVars: calcMetrics(detectedEnvVars, expectedEnvVars),
81
+ runtime,
82
+ };
83
+ // Components (if ground truth has them)
84
+ if (groundTruth.components && groundTruth.components.length > 0) {
85
+ const detectedComps = new Set(components.map((c) => c.name));
86
+ const expectedComps = new Set(groundTruth.components.map((c) => c.name));
87
+ result.components = calcMetrics(detectedComps, expectedComps);
88
+ }
89
+ // Middleware
90
+ if (groundTruth.middleware && groundTruth.middleware.length > 0) {
91
+ const detectedMw = new Set(middleware.map((m) => m.name));
92
+ const expectedMw = new Set(groundTruth.middleware);
93
+ result.middleware = calcMetrics(detectedMw, expectedMw);
94
+ }
95
+ return result;
96
+ }
97
+ finally {
98
+ // Cleanup temp dir
99
+ await rm(tmpDir, { recursive: true, force: true }).catch(() => { });
100
+ }
101
+ }
102
+ function formatPercent(n) {
103
+ return `${(n * 100).toFixed(1)}%`;
104
+ }
105
+ function printMetrics(label, m) {
106
+ console.log(` ${label.padEnd(14)} P: ${formatPercent(m.precision).padStart(6)} R: ${formatPercent(m.recall).padStart(6)} F1: ${formatPercent(m.f1).padStart(6)} (TP:${m.truePositives} FP:${m.falsePositives} FN:${m.falseNegatives})`);
107
+ }
108
+ export async function runEval() {
109
+ // Find eval fixtures
110
+ const __dirname = dirname(fileURLToPath(import.meta.url));
111
+ const evalDir = join(__dirname, "..", "eval", "fixtures");
112
+ let fixtureNames;
113
+ try {
114
+ const { readdir } = await import("node:fs/promises");
115
+ fixtureNames = await readdir(evalDir);
116
+ }
117
+ catch {
118
+ // Try from dist path
119
+ const altDir = join(__dirname, "..", "..", "eval", "fixtures");
120
+ const { readdir } = await import("node:fs/promises");
121
+ fixtureNames = await readdir(altDir);
122
+ // Override evalDir for the loop below
123
+ return runEvalFromDir(altDir, fixtureNames);
124
+ }
125
+ return runEvalFromDir(evalDir, fixtureNames);
126
+ }
127
+ async function runEvalFromDir(evalDir, fixtureNames) {
128
+ console.log(`\n codesight eval — precision/recall benchmarks\n`);
129
+ const results = [];
130
+ let totalPrecision = 0;
131
+ let totalRecall = 0;
132
+ let totalF1 = 0;
133
+ let metricCount = 0;
134
+ for (const name of fixtureNames) {
135
+ const fixturePath = join(evalDir, name);
136
+ // Check if it has repo.json
137
+ try {
138
+ await import("node:fs/promises").then((fs) => fs.stat(join(fixturePath, "repo.json")));
139
+ }
140
+ catch {
141
+ continue;
142
+ }
143
+ process.stdout.write(` ${name}...`);
144
+ const result = await evalFixture(fixturePath);
145
+ results.push(result);
146
+ console.log(` ${result.runtime}ms`);
147
+ printMetrics("Routes", result.routes);
148
+ printMetrics("Models", result.models);
149
+ printMetrics("Env vars", result.envVars);
150
+ if (result.components)
151
+ printMetrics("Components", result.components);
152
+ if (result.middleware)
153
+ printMetrics("Middleware", result.middleware);
154
+ console.log("");
155
+ // Accumulate for averages
156
+ const metrics = [result.routes, result.models, result.envVars];
157
+ if (result.components)
158
+ metrics.push(result.components);
159
+ if (result.middleware)
160
+ metrics.push(result.middleware);
161
+ for (const m of metrics) {
162
+ totalPrecision += m.precision;
163
+ totalRecall += m.recall;
164
+ totalF1 += m.f1;
165
+ metricCount++;
166
+ }
167
+ }
168
+ if (results.length === 0) {
169
+ console.log(" No fixtures found. Add fixtures to eval/fixtures/");
170
+ return;
171
+ }
172
+ // Summary
173
+ const avgP = totalPrecision / metricCount;
174
+ const avgR = totalRecall / metricCount;
175
+ const avgF1 = totalF1 / metricCount;
176
+ const totalRuntime = results.reduce((s, r) => s + r.runtime, 0);
177
+ console.log(" ──────────────────────────────────────────");
178
+ console.log(` Fixtures: ${results.length}`);
179
+ console.log(` Avg precision: ${formatPercent(avgP)}`);
180
+ console.log(` Avg recall: ${formatPercent(avgR)}`);
181
+ console.log(` Avg F1: ${formatPercent(avgF1)}`);
182
+ console.log(` Total runtime: ${totalRuntime}ms`);
183
+ console.log("");
184
+ }
package/dist/index.js CHANGED
@@ -14,7 +14,8 @@ import { calculateTokenStats } from "./detectors/tokens.js";
14
14
  import { writeOutput } from "./formatter.js";
15
15
  import { generateAIConfigs } from "./generators/ai-config.js";
16
16
  import { generateHtmlReport } from "./generators/html-report.js";
17
- const VERSION = "1.3.1";
17
+ import { loadConfig, mergeCliConfig } from "./config.js";
18
+ const VERSION = "1.4.0";
18
19
  const BRAND = "codesight";
19
20
  function printHelp() {
20
21
  console.log(`
@@ -35,9 +36,15 @@ function printHelp() {
35
36
  --benchmark Show detailed token savings breakdown
36
37
  --profile <tool> Generate optimized config (claude-code|cursor|codex|copilot|windsurf)
37
38
  --blast <file> Show blast radius for a file
39
+ --telemetry Run token telemetry (real before/after measurement)
40
+ --eval Run precision/recall benchmarks on eval fixtures
38
41
  -v, --version Show version
39
42
  -h, --help Show this help
40
43
 
44
+ Config:
45
+ Reads codesight.config.(ts|js|json) or package.json "codesight" field.
46
+ See docs for disableDetectors, customRoutePatterns, plugins, and more.
47
+
41
48
  Examples:
42
49
  npx ${BRAND} # Scan current directory
43
50
  npx ${BRAND} --init # Scan + generate AI config files
@@ -45,6 +52,8 @@ function printHelp() {
45
52
  npx ${BRAND} --watch # Watch mode, re-scan on changes
46
53
  npx ${BRAND} --mcp # Start MCP server
47
54
  npx ${BRAND} --hook # Install git pre-commit hook
55
+ npx ${BRAND} --telemetry # Measure real token savings
56
+ npx ${BRAND} --eval # Run accuracy benchmarks
48
57
  npx ${BRAND} ./my-project # Scan specific directory
49
58
  `);
50
59
  }
@@ -57,7 +66,7 @@ async function fileExists(path) {
57
66
  return false;
58
67
  }
59
68
  }
60
- async function scan(root, outputDirName, maxDepth) {
69
+ async function scan(root, outputDirName, maxDepth, userConfig = {}) {
61
70
  const outputDir = join(root, outputDirName);
62
71
  console.log(`\n ${BRAND} v${VERSION}`);
63
72
  console.log(` Scanning: ${root}\n`);
@@ -73,17 +82,39 @@ async function scan(root, outputDirName, maxDepth) {
73
82
  process.stdout.write(" Collecting files...");
74
83
  const files = await collectFiles(root, maxDepth);
75
84
  console.log(` ${files.length} files`);
76
- // Step 3: Run all detectors in parallel
85
+ // Step 3: Run all detectors in parallel (respecting disableDetectors config)
77
86
  process.stdout.write(" Analyzing...");
78
- const [rawRoutes, schemas, components, libs, config, middleware, graph] = await Promise.all([
79
- detectRoutes(files, project),
80
- detectSchemas(files, project),
81
- detectComponents(files, project),
82
- detectLibs(files, project),
83
- detectConfig(files, project),
84
- detectMiddleware(files, project),
85
- detectDependencyGraph(files, project),
87
+ const disabled = new Set(userConfig.disableDetectors || []);
88
+ const [rawRoutes, schemas, components, libs, configResult, middleware, graph] = await Promise.all([
89
+ disabled.has("routes") ? Promise.resolve([]) : detectRoutes(files, project),
90
+ disabled.has("schema") ? Promise.resolve([]) : detectSchemas(files, project),
91
+ disabled.has("components") ? Promise.resolve([]) : detectComponents(files, project),
92
+ disabled.has("libs") ? Promise.resolve([]) : detectLibs(files, project),
93
+ disabled.has("config") ? Promise.resolve({ envVars: [], configFiles: [], dependencies: {}, devDependencies: {} }) : detectConfig(files, project),
94
+ disabled.has("middleware") ? Promise.resolve([]) : detectMiddleware(files, project),
95
+ disabled.has("graph") ? Promise.resolve({ edges: [], hotFiles: [] }) : detectDependencyGraph(files, project),
86
96
  ]);
97
+ // Step 3b: Run plugin detectors
98
+ if (userConfig.plugins) {
99
+ for (const plugin of userConfig.plugins) {
100
+ if (plugin.detector) {
101
+ try {
102
+ const pluginResult = await plugin.detector(files, project);
103
+ if (pluginResult.routes)
104
+ rawRoutes.push(...pluginResult.routes);
105
+ if (pluginResult.schemas)
106
+ schemas.push(...pluginResult.schemas);
107
+ if (pluginResult.components)
108
+ components.push(...pluginResult.components);
109
+ if (pluginResult.middleware)
110
+ middleware.push(...pluginResult.middleware);
111
+ }
112
+ catch (err) {
113
+ console.warn(`\n Warning: plugin "${plugin.name}" failed: ${err.message}`);
114
+ }
115
+ }
116
+ }
117
+ }
87
118
  // Step 4: Enrich routes with contract info
88
119
  const routes = await enrichRouteContracts(rawRoutes, project);
89
120
  // Report AST vs regex detection
@@ -106,7 +137,7 @@ async function scan(root, outputDirName, maxDepth) {
106
137
  schemas,
107
138
  components,
108
139
  libs,
109
- config,
140
+ config: configResult,
110
141
  middleware,
111
142
  graph,
112
143
  tokenStats: { outputTokens: 0, estimatedExplorationTokens: 0, saved: 0, fileCount: files.length },
@@ -126,7 +157,7 @@ async function scan(root, outputDirName, maxDepth) {
126
157
  Models: ${schemas.length}
127
158
  Components: ${components.length}
128
159
  Libraries: ${libs.length}
129
- Env vars: ${config.envVars.length}
160
+ Env vars: ${configResult.envVars.length}
130
161
  Middleware: ${middleware.length}
131
162
  Import links: ${graph.edges.length}
132
163
  Hot files: ${graph.hotFiles.length}
@@ -234,6 +265,8 @@ async function main() {
234
265
  let doBenchmark = false;
235
266
  let doProfile = "";
236
267
  let doBlast = "";
268
+ let doTelemetry = false;
269
+ let doEval = false;
237
270
  for (let i = 0; i < args.length; i++) {
238
271
  const arg = args[i];
239
272
  if ((arg === "-o" || arg === "--output") && args[i + 1]) {
@@ -273,6 +306,12 @@ async function main() {
273
306
  else if (arg === "--blast" && args[i + 1]) {
274
307
  doBlast = args[++i];
275
308
  }
309
+ else if (arg === "--telemetry") {
310
+ doTelemetry = true;
311
+ }
312
+ else if (arg === "--eval") {
313
+ doEval = true;
314
+ }
276
315
  else if (!arg.startsWith("-")) {
277
316
  targetDir = resolve(arg);
278
317
  }
@@ -283,13 +322,58 @@ async function main() {
283
322
  await startMCPServer();
284
323
  return;
285
324
  }
325
+ // Eval mode (standalone, no scan needed)
326
+ if (doEval) {
327
+ const { runEval } = await import("./eval.js");
328
+ await runEval();
329
+ return;
330
+ }
286
331
  const root = resolve(targetDir);
332
+ // Load config file
333
+ const fileConfig = await loadConfig(root);
334
+ const config = mergeCliConfig(fileConfig, {
335
+ maxDepth: maxDepth !== 10 ? maxDepth : undefined,
336
+ outputDir: outputDirName !== ".codesight" ? outputDirName : undefined,
337
+ profile: doProfile || undefined,
338
+ });
339
+ // Apply config overrides
340
+ if (config.maxDepth)
341
+ maxDepth = config.maxDepth;
342
+ if (config.outputDir)
343
+ outputDirName = config.outputDir;
287
344
  // Install git hook
288
345
  if (doHook) {
289
346
  await installGitHook(root, outputDirName);
290
347
  }
291
- // Run scan
292
- const result = await scan(root, outputDirName, maxDepth);
348
+ // Run scan (passes config for disabled detectors + plugins)
349
+ let result = await scan(root, outputDirName, maxDepth, config);
350
+ // Run plugin post-processors
351
+ if (config.plugins) {
352
+ for (const plugin of config.plugins) {
353
+ if (plugin.postProcessor) {
354
+ try {
355
+ result = await plugin.postProcessor(result);
356
+ }
357
+ catch (err) {
358
+ console.warn(` Warning: plugin "${plugin.name}" post-processor failed: ${err.message}`);
359
+ }
360
+ }
361
+ }
362
+ }
363
+ // Token telemetry
364
+ if (doTelemetry) {
365
+ const { runTelemetry } = await import("./telemetry.js");
366
+ const outputDir = join(root, outputDirName);
367
+ process.stdout.write(" Running telemetry...");
368
+ const report = await runTelemetry(root, result, outputDir);
369
+ console.log(` ${outputDirName}/telemetry.md`);
370
+ console.log(`\n Telemetry Results:`);
371
+ for (const task of report.tasks) {
372
+ console.log(` ${task.name}: ${task.reduction}x reduction (${task.tokensWithout.toLocaleString()} → ${task.tokensWith.toLocaleString()} tokens)`);
373
+ }
374
+ console.log(` Average: ${report.summary.averageReduction}x | Tool calls saved: ${report.summary.totalToolCallsSaved}`);
375
+ console.log("");
376
+ }
293
377
  // JSON output
294
378
  if (jsonOutput) {
295
379
  console.log(JSON.stringify(result, null, 2));
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Token telemetry: measures real before/after token usage by simulating
3
+ * what an AI agent would do with and without codesight context.
4
+ *
5
+ * Approach: for each standard task (explain architecture, add route, review diff),
6
+ * measure the actual bytes of context that would be consumed.
7
+ *
8
+ * "Without codesight": count tokens from the files an AI would need to read
9
+ * to discover routes, schema, components, config, etc.
10
+ *
11
+ * "With codesight": count tokens from the CODESIGHT.md output.
12
+ */
13
+ import type { ScanResult } from "./types.js";
14
+ export interface TelemetryTask {
15
+ name: string;
16
+ description: string;
17
+ /** Files the AI would need to read without codesight */
18
+ filesRead: string[];
19
+ /** Tool calls the AI would make (glob, grep, read) */
20
+ toolCalls: number;
21
+ /** Tokens consumed reading those files */
22
+ tokensWithout: number;
23
+ /** Tokens consumed from codesight output */
24
+ tokensWith: number;
25
+ /** Reduction factor */
26
+ reduction: number;
27
+ }
28
+ export interface TelemetryReport {
29
+ project: string;
30
+ tasks: TelemetryTask[];
31
+ summary: {
32
+ totalTokensWithout: number;
33
+ totalTokensWith: number;
34
+ averageReduction: number;
35
+ totalToolCallsSaved: number;
36
+ };
37
+ }
38
+ export declare function runTelemetry(root: string, result: ScanResult, outputDir: string): Promise<TelemetryReport>;
@@ -0,0 +1,257 @@
1
+ /**
2
+ * Token telemetry: measures real before/after token usage by simulating
3
+ * what an AI agent would do with and without codesight context.
4
+ *
5
+ * Approach: for each standard task (explain architecture, add route, review diff),
6
+ * measure the actual bytes of context that would be consumed.
7
+ *
8
+ * "Without codesight": count tokens from the files an AI would need to read
9
+ * to discover routes, schema, components, config, etc.
10
+ *
11
+ * "With codesight": count tokens from the CODESIGHT.md output.
12
+ */
13
+ import { readFile } from "node:fs/promises";
14
+ import { join, relative } from "node:path";
15
+ function countTokens(text) {
16
+ return Math.ceil(text.length / 4);
17
+ }
18
+ async function readFileSafe(path) {
19
+ try {
20
+ return await readFile(path, "utf-8");
21
+ }
22
+ catch {
23
+ return "";
24
+ }
25
+ }
26
+ /**
27
+ * Task 1: "Explain the architecture"
28
+ * Without codesight: AI reads package.json, scans dirs, reads route files,
29
+ * schema files, config files, middleware files — typically 15-25 file reads.
30
+ */
31
+ async function measureExplainArchitecture(root, result, codesightTokens) {
32
+ const filesToRead = new Set();
33
+ // AI would read package.json first
34
+ filesToRead.add(join(root, "package.json"));
35
+ // Then scan for route files
36
+ for (const route of result.routes) {
37
+ filesToRead.add(join(root, route.file));
38
+ }
39
+ // Schema files
40
+ for (const _schema of result.schemas) {
41
+ // Find the file containing this schema from routes or libs
42
+ for (const lib of result.libs) {
43
+ if (lib.file.includes("schema") || lib.file.includes("model") || lib.file.includes("db")) {
44
+ filesToRead.add(join(root, lib.file));
45
+ }
46
+ }
47
+ }
48
+ // Config files
49
+ for (const cf of result.config.configFiles) {
50
+ filesToRead.add(join(root, cf));
51
+ }
52
+ // Middleware files
53
+ for (const mw of result.middleware) {
54
+ filesToRead.add(join(root, mw.file));
55
+ }
56
+ // Hot files (AI would discover these during exploration)
57
+ for (const hf of result.graph.hotFiles.slice(0, 10)) {
58
+ filesToRead.add(join(root, hf.file));
59
+ }
60
+ // Read all files and count tokens
61
+ let totalTokens = 0;
62
+ const readFiles = [];
63
+ for (const f of filesToRead) {
64
+ const content = await readFileSafe(f);
65
+ if (content) {
66
+ totalTokens += countTokens(content);
67
+ readFiles.push(relative(root, f));
68
+ }
69
+ }
70
+ // Add overhead for glob/grep tool calls (each costs ~50-100 tokens for command + results)
71
+ const toolCalls = Math.max(10, Math.ceil(filesToRead.size * 0.8));
72
+ totalTokens += toolCalls * 75; // average 75 tokens per tool call overhead
73
+ const reduction = totalTokens > 0 ? Math.round((totalTokens / codesightTokens) * 10) / 10 : 1;
74
+ return {
75
+ name: "Explain architecture",
76
+ description: "Understand project stack, routes, schema, and dependencies",
77
+ filesRead: readFiles,
78
+ toolCalls,
79
+ tokensWithout: totalTokens,
80
+ tokensWith: codesightTokens,
81
+ reduction,
82
+ };
83
+ }
84
+ /**
85
+ * Task 2: "Add a new API route"
86
+ * Without codesight: AI needs to find existing routes to match patterns,
87
+ * read schema for related models, check middleware, check config.
88
+ */
89
+ async function measureAddRoute(root, result, codesightTokens) {
90
+ const filesToRead = new Set();
91
+ // AI would grep for existing route patterns — reads 3-5 route files
92
+ const routeFiles = [...new Set(result.routes.map((r) => r.file))];
93
+ for (const f of routeFiles.slice(0, 5)) {
94
+ filesToRead.add(join(root, f));
95
+ }
96
+ // Read schema to understand models
97
+ for (const lib of result.libs) {
98
+ if (lib.file.includes("schema") || lib.file.includes("model") || lib.file.includes("db")) {
99
+ filesToRead.add(join(root, lib.file));
100
+ }
101
+ }
102
+ // Check middleware to know what to apply
103
+ for (const mw of result.middleware) {
104
+ filesToRead.add(join(root, mw.file));
105
+ }
106
+ let totalTokens = 0;
107
+ const readFiles = [];
108
+ for (const f of filesToRead) {
109
+ const content = await readFileSafe(f);
110
+ if (content) {
111
+ totalTokens += countTokens(content);
112
+ readFiles.push(relative(root, f));
113
+ }
114
+ }
115
+ const toolCalls = Math.max(6, Math.ceil(filesToRead.size * 0.7));
116
+ totalTokens += toolCalls * 75;
117
+ // With codesight, AI only reads the routes + schema sections (~40% of output)
118
+ const withTokens = Math.ceil(codesightTokens * 0.4);
119
+ const reduction = totalTokens > 0 ? Math.round((totalTokens / withTokens) * 10) / 10 : 1;
120
+ return {
121
+ name: "Add new API route",
122
+ description: "Find route patterns, check schema, apply middleware",
123
+ filesRead: readFiles,
124
+ toolCalls,
125
+ tokensWithout: totalTokens,
126
+ tokensWith: withTokens,
127
+ reduction,
128
+ };
129
+ }
130
+ /**
131
+ * Task 3: "Review a diff / understand blast radius"
132
+ * Without codesight: AI needs to trace imports, find dependents, check what routes
133
+ * and models are affected by a file change.
134
+ */
135
+ async function measureReviewDiff(root, result, codesightTokens) {
136
+ const filesToRead = new Set();
137
+ // AI would read the changed file + all its importers
138
+ // Simulate: pick the hottest file and trace its dependents
139
+ if (result.graph.hotFiles.length > 0) {
140
+ const hotFile = result.graph.hotFiles[0];
141
+ filesToRead.add(join(root, hotFile.file));
142
+ // Read files that import it
143
+ for (const edge of result.graph.edges) {
144
+ if (edge.to === hotFile.file) {
145
+ filesToRead.add(join(root, edge.from));
146
+ }
147
+ }
148
+ }
149
+ // Also read some route files to check impact
150
+ const routeFiles = [...new Set(result.routes.map((r) => r.file))];
151
+ for (const f of routeFiles.slice(0, 3)) {
152
+ filesToRead.add(join(root, f));
153
+ }
154
+ let totalTokens = 0;
155
+ const readFiles = [];
156
+ for (const f of filesToRead) {
157
+ const content = await readFileSafe(f);
158
+ if (content) {
159
+ totalTokens += countTokens(content);
160
+ readFiles.push(relative(root, f));
161
+ }
162
+ }
163
+ const toolCalls = Math.max(8, Math.ceil(filesToRead.size * 0.6));
164
+ totalTokens += toolCalls * 75;
165
+ // With codesight, AI reads graph section + routes (~50% of output)
166
+ const withTokens = Math.ceil(codesightTokens * 0.5);
167
+ const reduction = totalTokens > 0 ? Math.round((totalTokens / withTokens) * 10) / 10 : 1;
168
+ return {
169
+ name: "Review diff / blast radius",
170
+ description: "Trace imports, find affected routes and models",
171
+ filesRead: readFiles,
172
+ toolCalls,
173
+ tokensWithout: totalTokens,
174
+ tokensWith: withTokens,
175
+ reduction,
176
+ };
177
+ }
178
+ export async function runTelemetry(root, result, outputDir) {
179
+ // Read the codesight output to get real token count
180
+ const codesightContent = await readFileSafe(join(outputDir, "CODESIGHT.md"));
181
+ const codesightTokens = countTokens(codesightContent);
182
+ const tasks = await Promise.all([
183
+ measureExplainArchitecture(root, result, codesightTokens),
184
+ measureAddRoute(root, result, codesightTokens),
185
+ measureReviewDiff(root, result, codesightTokens),
186
+ ]);
187
+ const totalWithout = tasks.reduce((s, t) => s + t.tokensWithout, 0);
188
+ const totalWith = tasks.reduce((s, t) => s + t.tokensWith, 0);
189
+ const totalToolCalls = tasks.reduce((s, t) => s + t.toolCalls, 0);
190
+ const report = {
191
+ project: result.project.name,
192
+ tasks,
193
+ summary: {
194
+ totalTokensWithout: totalWithout,
195
+ totalTokensWith: totalWith,
196
+ averageReduction: totalWith > 0 ? Math.round((totalWithout / totalWith) * 10) / 10 : 1,
197
+ totalToolCallsSaved: totalToolCalls,
198
+ },
199
+ };
200
+ // Write telemetry report
201
+ const reportLines = [
202
+ `# Token Telemetry: ${result.project.name}`,
203
+ "",
204
+ `> Measured by reading the actual files an AI agent would need for each task,`,
205
+ `> then comparing against the codesight output (~${codesightTokens.toLocaleString()} tokens).`,
206
+ "",
207
+ "## Tasks",
208
+ "",
209
+ ];
210
+ for (const task of tasks) {
211
+ reportLines.push(`### ${task.name}`);
212
+ reportLines.push(`_${task.description}_`);
213
+ reportLines.push("");
214
+ reportLines.push(`| Metric | Value |`);
215
+ reportLines.push(`|---|---|`);
216
+ reportLines.push(`| Files AI would read | ${task.filesRead.length} |`);
217
+ reportLines.push(`| Tool calls (glob/grep/read) | ${task.toolCalls} |`);
218
+ reportLines.push(`| Tokens without codesight | ~${task.tokensWithout.toLocaleString()} |`);
219
+ reportLines.push(`| Tokens with codesight | ~${task.tokensWith.toLocaleString()} |`);
220
+ reportLines.push(`| **Reduction** | **${task.reduction}x** |`);
221
+ reportLines.push("");
222
+ if (task.filesRead.length > 0) {
223
+ reportLines.push("<details>");
224
+ reportLines.push(`<summary>Files read (${task.filesRead.length})</summary>`);
225
+ reportLines.push("");
226
+ for (const f of task.filesRead) {
227
+ reportLines.push(`- \`${f}\``);
228
+ }
229
+ reportLines.push("");
230
+ reportLines.push("</details>");
231
+ reportLines.push("");
232
+ }
233
+ }
234
+ reportLines.push("## Summary");
235
+ reportLines.push("");
236
+ reportLines.push(`| Metric | Value |`);
237
+ reportLines.push(`|---|---|`);
238
+ reportLines.push(`| Total tokens without codesight | ~${report.summary.totalTokensWithout.toLocaleString()} |`);
239
+ reportLines.push(`| Total tokens with codesight | ~${report.summary.totalTokensWith.toLocaleString()} |`);
240
+ reportLines.push(`| **Average reduction** | **${report.summary.averageReduction}x** |`);
241
+ reportLines.push(`| Tool calls saved | ${report.summary.totalToolCallsSaved} |`);
242
+ reportLines.push("");
243
+ reportLines.push("## Methodology");
244
+ reportLines.push("");
245
+ reportLines.push("Token counts are calculated by reading the actual source files an AI agent would");
246
+ reportLines.push("need to explore for each task, using the ~4 chars/token heuristic (standard for");
247
+ reportLines.push("GPT/Claude tokenizers). Tool call overhead is estimated at ~75 tokens per call");
248
+ reportLines.push("(command text + result formatting). The \"with codesight\" count uses the real");
249
+ reportLines.push("CODESIGHT.md output size, proportioned to the sections relevant to each task.");
250
+ reportLines.push("");
251
+ reportLines.push(`_Generated by codesight --telemetry_`);
252
+ const { writeFile: wf } = await import("node:fs/promises");
253
+ const { mkdir } = await import("node:fs/promises");
254
+ await mkdir(outputDir, { recursive: true });
255
+ await wf(join(outputDir, "telemetry.md"), reportLines.join("\n"));
256
+ return report;
257
+ }