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/README.md +252 -103
- package/dist/config.d.ts +16 -0
- package/dist/config.js +96 -0
- package/dist/detectors/schema.js +1 -1
- package/dist/eval.d.ts +5 -0
- package/dist/eval.js +184 -0
- package/dist/index.js +99 -15
- package/dist/telemetry.d.ts +38 -0
- package/dist/telemetry.js +257 -0
- package/dist/types.d.ts +35 -0
- package/eval/README.md +36 -0
- package/eval/fixtures/express-prisma/ground-truth.json +31 -0
- package/eval/fixtures/express-prisma/repo.json +19 -0
- package/eval/fixtures/fastapi-sqlalchemy/ground-truth.json +29 -0
- package/eval/fixtures/fastapi-sqlalchemy/repo.json +16 -0
- package/eval/fixtures/hono-monorepo/ground-truth.json +33 -0
- package/eval/fixtures/hono-monorepo/repo.json +20 -0
- package/eval/fixtures/nextjs-drizzle/ground-truth.json +37 -0
- package/eval/fixtures/nextjs-drizzle/repo.json +21 -0
- package/package.json +3 -2
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
|
-
|
|
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
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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: ${
|
|
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
|
-
|
|
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
|
+
}
|