@aiready/agent-grounding 0.1.5 → 0.1.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/.turbo/turbo-build.log +8 -8
- package/.turbo/turbo-lint.log +5 -0
- package/.turbo/turbo-test.log +5 -5
- package/dist/chunk-NHDH733I.mjs +336 -0
- package/dist/chunk-NXIMJNCK.mjs +294 -0
- package/dist/chunk-ZOE5BFWE.mjs +339 -0
- package/dist/cli.js +90 -78
- package/dist/cli.mjs +43 -11
- package/dist/index.js +53 -69
- package/dist/index.mjs +1 -1
- package/package.json +3 -3
- package/src/__tests__/analyzer.test.ts +30 -10
- package/src/analyzer.ts +83 -82
- package/src/cli.ts +58 -17
- package/src/scoring.ts +14 -9
- package/src/types.ts +6 -1
package/dist/cli.mjs
CHANGED
|
@@ -2,16 +2,24 @@
|
|
|
2
2
|
import {
|
|
3
3
|
analyzeAgentGrounding,
|
|
4
4
|
calculateGroundingScore
|
|
5
|
-
} from "./chunk-
|
|
5
|
+
} from "./chunk-NXIMJNCK.mjs";
|
|
6
6
|
|
|
7
7
|
// src/cli.ts
|
|
8
8
|
import { Command } from "commander";
|
|
9
9
|
import chalk from "chalk";
|
|
10
10
|
import { writeFileSync, mkdirSync, existsSync } from "fs";
|
|
11
11
|
import { dirname } from "path";
|
|
12
|
-
import {
|
|
12
|
+
import {
|
|
13
|
+
loadConfig,
|
|
14
|
+
mergeConfigWithDefaults,
|
|
15
|
+
resolveOutputPath
|
|
16
|
+
} from "@aiready/core";
|
|
13
17
|
var program = new Command();
|
|
14
|
-
program.name("aiready-agent-grounding").description(
|
|
18
|
+
program.name("aiready-agent-grounding").description(
|
|
19
|
+
"Measure how well an AI agent can navigate your codebase autonomously"
|
|
20
|
+
).version("0.1.0").addHelpText(
|
|
21
|
+
"after",
|
|
22
|
+
`
|
|
15
23
|
GROUNDING DIMENSIONS:
|
|
16
24
|
Structure Clarity Deep directory trees slow and confuse agents
|
|
17
25
|
Self-Documentation Vague file names (utils, helpers) hide intent
|
|
@@ -23,7 +31,16 @@ EXAMPLES:
|
|
|
23
31
|
aiready-agent-grounding . # Full analysis
|
|
24
32
|
aiready-agent-grounding src/ --output json # JSON report
|
|
25
33
|
aiready-agent-grounding . --max-depth 3 # Stricter depth limit
|
|
26
|
-
`
|
|
34
|
+
`
|
|
35
|
+
).argument("<directory>", "Directory to analyze").option(
|
|
36
|
+
"--max-depth <n>",
|
|
37
|
+
"Max recommended directory depth (default: 4)",
|
|
38
|
+
"4"
|
|
39
|
+
).option(
|
|
40
|
+
"--readme-stale-days <n>",
|
|
41
|
+
"Days after which README is considered stale (default: 90)",
|
|
42
|
+
"90"
|
|
43
|
+
).option("--include <patterns>", "File patterns to include (comma-separated)").option("--exclude <patterns>", "File patterns to exclude (comma-separated)").option("-o, --output <format>", "Output format: console|json", "console").option("--output-file <path>", "Output file path (for json)").action(async (directory, options) => {
|
|
27
44
|
console.log(chalk.blue("\u{1F9ED} Analyzing agent grounding...\n"));
|
|
28
45
|
const startTime = Date.now();
|
|
29
46
|
const config = await loadConfig(directory);
|
|
@@ -65,10 +82,14 @@ function scoreColor(score) {
|
|
|
65
82
|
return chalk.bgRed.white;
|
|
66
83
|
}
|
|
67
84
|
function displayConsoleReport(report, scoring, elapsed) {
|
|
68
|
-
const { summary,
|
|
85
|
+
const { summary, issues, recommendations } = report;
|
|
69
86
|
console.log(chalk.bold("\n\u{1F9ED} Agent Grounding Analysis\n"));
|
|
70
|
-
console.log(
|
|
71
|
-
|
|
87
|
+
console.log(
|
|
88
|
+
`Score: ${scoreColor(summary.score)(summary.score + "/100")} (${summary.rating.toUpperCase()})`
|
|
89
|
+
);
|
|
90
|
+
console.log(
|
|
91
|
+
`Files: ${chalk.cyan(summary.filesAnalyzed)} Directories: ${chalk.cyan(summary.directoriesAnalyzed)}`
|
|
92
|
+
);
|
|
72
93
|
console.log(`Analysis: ${chalk.gray(elapsed + "s")}
|
|
73
94
|
`);
|
|
74
95
|
console.log(chalk.bold("\u{1F4D0} Dimension Scores\n"));
|
|
@@ -81,22 +102,33 @@ function displayConsoleReport(report, scoring, elapsed) {
|
|
|
81
102
|
];
|
|
82
103
|
for (const [name, val] of dims) {
|
|
83
104
|
const bar = "\u2588".repeat(Math.round(val / 10)).padEnd(10, "\u2591");
|
|
84
|
-
console.log(
|
|
105
|
+
console.log(
|
|
106
|
+
` ${String(name).padEnd(22)} ${scoreColor(val)(bar)} ${val}/100`
|
|
107
|
+
);
|
|
85
108
|
}
|
|
86
109
|
if (issues.length > 0) {
|
|
87
110
|
console.log(chalk.bold("\n\u26A0\uFE0F Issues Found\n"));
|
|
88
111
|
for (const issue of issues) {
|
|
89
112
|
const sev = issue.severity === "critical" ? chalk.red : issue.severity === "major" ? chalk.yellow : chalk.blue;
|
|
90
113
|
console.log(`${sev(issue.severity.toUpperCase())} ${issue.message}`);
|
|
91
|
-
if (issue.suggestion)
|
|
114
|
+
if (issue.suggestion)
|
|
115
|
+
console.log(
|
|
116
|
+
` ${chalk.dim("\u2192")} ${chalk.italic(issue.suggestion)}`
|
|
117
|
+
);
|
|
92
118
|
console.log();
|
|
93
119
|
}
|
|
94
120
|
} else {
|
|
95
|
-
console.log(
|
|
121
|
+
console.log(
|
|
122
|
+
chalk.green(
|
|
123
|
+
"\n\u2728 No grounding issues found \u2014 agents can navigate freely!\n"
|
|
124
|
+
)
|
|
125
|
+
);
|
|
96
126
|
}
|
|
97
127
|
if (recommendations.length > 0) {
|
|
98
128
|
console.log(chalk.bold("\u{1F4A1} Recommendations\n"));
|
|
99
|
-
recommendations.forEach(
|
|
129
|
+
recommendations.forEach(
|
|
130
|
+
(rec, i) => console.log(`${i + 1}. ${rec}`)
|
|
131
|
+
);
|
|
100
132
|
}
|
|
101
133
|
console.log();
|
|
102
134
|
}
|
package/dist/index.js
CHANGED
|
@@ -26,69 +26,22 @@ __export(index_exports, {
|
|
|
26
26
|
module.exports = __toCommonJS(index_exports);
|
|
27
27
|
|
|
28
28
|
// src/analyzer.ts
|
|
29
|
+
var import_core = require("@aiready/core");
|
|
29
30
|
var import_fs = require("fs");
|
|
30
31
|
var import_path = require("path");
|
|
31
32
|
var import_typescript_estree = require("@typescript-eslint/typescript-estree");
|
|
32
|
-
var import_core = require("@aiready/core");
|
|
33
|
-
var VAGUE_FILE_NAMES = /* @__PURE__ */ new Set([
|
|
34
|
-
"utils",
|
|
35
|
-
"helpers",
|
|
36
|
-
"helper",
|
|
37
|
-
"misc",
|
|
38
|
-
"common",
|
|
39
|
-
"shared",
|
|
40
|
-
"tools",
|
|
41
|
-
"util",
|
|
42
|
-
"lib",
|
|
43
|
-
"libs",
|
|
44
|
-
"stuff",
|
|
45
|
-
"functions",
|
|
46
|
-
"methods",
|
|
47
|
-
"handlers",
|
|
48
|
-
"data",
|
|
49
|
-
"temp",
|
|
50
|
-
"tmp",
|
|
51
|
-
"test-utils",
|
|
52
|
-
"test-helpers",
|
|
53
|
-
"mocks"
|
|
54
|
-
]);
|
|
55
|
-
var SUPPORTED_EXTENSIONS = /* @__PURE__ */ new Set([".ts", ".tsx", ".js", ".jsx"]);
|
|
56
|
-
var DEFAULT_EXCLUDES = ["node_modules", "dist", ".git", "coverage", ".turbo", "build"];
|
|
57
|
-
function collectEntries(dir, options, depth = 0, dirs = [], files = []) {
|
|
58
|
-
if (depth > (options.maxDepth ?? 20)) return { dirs, files };
|
|
59
|
-
const excludes = [...DEFAULT_EXCLUDES, ...options.exclude ?? []];
|
|
60
|
-
let entries;
|
|
61
|
-
try {
|
|
62
|
-
entries = (0, import_fs.readdirSync)(dir);
|
|
63
|
-
} catch {
|
|
64
|
-
return { dirs, files };
|
|
65
|
-
}
|
|
66
|
-
for (const entry of entries) {
|
|
67
|
-
if (excludes.some((ex) => entry === ex || entry.includes(ex))) continue;
|
|
68
|
-
const full = (0, import_path.join)(dir, entry);
|
|
69
|
-
let stat;
|
|
70
|
-
try {
|
|
71
|
-
stat = (0, import_fs.statSync)(full);
|
|
72
|
-
} catch {
|
|
73
|
-
continue;
|
|
74
|
-
}
|
|
75
|
-
if (stat.isDirectory()) {
|
|
76
|
-
dirs.push({ path: full, depth });
|
|
77
|
-
collectEntries(full, options, depth + 1, dirs, files);
|
|
78
|
-
} else if (stat.isFile() && SUPPORTED_EXTENSIONS.has((0, import_path.extname)(full))) {
|
|
79
|
-
if (!options.include || options.include.some((p) => full.includes(p))) {
|
|
80
|
-
files.push(full);
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
return { dirs, files };
|
|
85
|
-
}
|
|
86
33
|
function analyzeFile(filePath) {
|
|
87
34
|
let code;
|
|
88
35
|
try {
|
|
89
36
|
code = (0, import_fs.readFileSync)(filePath, "utf-8");
|
|
90
37
|
} catch {
|
|
91
|
-
return {
|
|
38
|
+
return {
|
|
39
|
+
isBarrel: false,
|
|
40
|
+
exportedNames: [],
|
|
41
|
+
untypedExports: 0,
|
|
42
|
+
totalExports: 0,
|
|
43
|
+
domainTerms: []
|
|
44
|
+
};
|
|
92
45
|
}
|
|
93
46
|
let ast;
|
|
94
47
|
try {
|
|
@@ -98,10 +51,16 @@ function analyzeFile(filePath) {
|
|
|
98
51
|
loc: false
|
|
99
52
|
});
|
|
100
53
|
} catch {
|
|
101
|
-
return {
|
|
54
|
+
return {
|
|
55
|
+
isBarrel: false,
|
|
56
|
+
exportedNames: [],
|
|
57
|
+
untypedExports: 0,
|
|
58
|
+
totalExports: 0,
|
|
59
|
+
domainTerms: []
|
|
60
|
+
};
|
|
102
61
|
}
|
|
103
62
|
let isBarrel = false;
|
|
104
|
-
|
|
63
|
+
const exportedNames = [];
|
|
105
64
|
let untypedExports = 0;
|
|
106
65
|
let totalExports = 0;
|
|
107
66
|
const domainTerms = [];
|
|
@@ -117,7 +76,9 @@ function analyzeFile(filePath) {
|
|
|
117
76
|
const name = decl.id?.name ?? decl.declarations?.[0]?.id?.name;
|
|
118
77
|
if (name) {
|
|
119
78
|
exportedNames.push(name);
|
|
120
|
-
domainTerms.push(
|
|
79
|
+
domainTerms.push(
|
|
80
|
+
...name.replace(/([A-Z])/g, " $1").toLowerCase().split(/\s+/).filter(Boolean)
|
|
81
|
+
);
|
|
121
82
|
const hasType = decl.returnType != null || decl.declarations?.[0]?.id?.typeAnnotation != null || decl.typeParameters != null;
|
|
122
83
|
if (!hasType) untypedExports++;
|
|
123
84
|
}
|
|
@@ -148,13 +109,24 @@ async function analyzeAgentGrounding(options) {
|
|
|
148
109
|
const rootDir = options.rootDir;
|
|
149
110
|
const maxRecommendedDepth = options.maxRecommendedDepth ?? 4;
|
|
150
111
|
const readmeStaleDays = options.readmeStaleDays ?? 90;
|
|
151
|
-
const {
|
|
152
|
-
|
|
153
|
-
|
|
112
|
+
const { files, dirs: rawDirs } = await (0, import_core.scanEntries)({
|
|
113
|
+
...options,
|
|
114
|
+
include: options.include || ["**/*.{ts,tsx,js,jsx}"]
|
|
115
|
+
});
|
|
116
|
+
const dirs = rawDirs.map((d) => ({
|
|
117
|
+
path: d,
|
|
118
|
+
depth: (0, import_path.relative)(rootDir, d).split(/[/\\]/).filter(Boolean).length
|
|
119
|
+
}));
|
|
120
|
+
const deepDirectories = dirs.filter(
|
|
121
|
+
(d) => d.depth > maxRecommendedDepth
|
|
122
|
+
).length;
|
|
123
|
+
const additionalVague = new Set(
|
|
124
|
+
(options.additionalVagueNames ?? []).map((n) => n.toLowerCase())
|
|
125
|
+
);
|
|
154
126
|
let vagueFileNames = 0;
|
|
155
127
|
for (const f of files) {
|
|
156
128
|
const base = (0, import_path.basename)(f, (0, import_path.extname)(f)).toLowerCase();
|
|
157
|
-
if (VAGUE_FILE_NAMES.has(base) || additionalVague.has(base)) {
|
|
129
|
+
if (import_core.VAGUE_FILE_NAMES.has(base) || additionalVague.has(base)) {
|
|
158
130
|
vagueFileNames++;
|
|
159
131
|
}
|
|
160
132
|
}
|
|
@@ -173,14 +145,24 @@ async function analyzeAgentGrounding(options) {
|
|
|
173
145
|
let barrelExports = 0;
|
|
174
146
|
let untypedExports = 0;
|
|
175
147
|
let totalExports = 0;
|
|
148
|
+
let processed = 0;
|
|
176
149
|
for (const f of files) {
|
|
150
|
+
processed++;
|
|
151
|
+
options.onProgress?.(
|
|
152
|
+
processed,
|
|
153
|
+
files.length,
|
|
154
|
+
`agent-grounding: analyzing files`
|
|
155
|
+
);
|
|
177
156
|
const analysis = analyzeFile(f);
|
|
178
157
|
if (analysis.isBarrel) barrelExports++;
|
|
179
158
|
untypedExports += analysis.untypedExports;
|
|
180
159
|
totalExports += analysis.totalExports;
|
|
181
160
|
allDomainTerms.push(...analysis.domainTerms);
|
|
182
161
|
}
|
|
183
|
-
const {
|
|
162
|
+
const {
|
|
163
|
+
inconsistent: inconsistentDomainTerms,
|
|
164
|
+
vocabularySize: domainVocabularySize
|
|
165
|
+
} = detectInconsistentTerms(allDomainTerms);
|
|
184
166
|
const groundingResult = (0, import_core.calculateAgentGrounding)({
|
|
185
167
|
deepDirectories,
|
|
186
168
|
totalDirectories: dirs.length,
|
|
@@ -282,7 +264,7 @@ async function analyzeAgentGrounding(options) {
|
|
|
282
264
|
|
|
283
265
|
// src/scoring.ts
|
|
284
266
|
function calculateGroundingScore(report) {
|
|
285
|
-
const { summary, rawData,
|
|
267
|
+
const { summary, rawData, recommendations } = report;
|
|
286
268
|
const factors = [
|
|
287
269
|
{
|
|
288
270
|
name: "Structure Clarity",
|
|
@@ -310,11 +292,13 @@ function calculateGroundingScore(report) {
|
|
|
310
292
|
description: `${rawData.inconsistentDomainTerms} inconsistent domain terms detected`
|
|
311
293
|
}
|
|
312
294
|
];
|
|
313
|
-
const recs = recommendations.map(
|
|
314
|
-
action
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
295
|
+
const recs = recommendations.map(
|
|
296
|
+
(action) => ({
|
|
297
|
+
action,
|
|
298
|
+
estimatedImpact: 6,
|
|
299
|
+
priority: summary.score < 50 ? "high" : "medium"
|
|
300
|
+
})
|
|
301
|
+
);
|
|
318
302
|
return {
|
|
319
303
|
toolName: "agent-grounding",
|
|
320
304
|
score: summary.score,
|
package/dist/index.mjs
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aiready/agent-grounding",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.8",
|
|
4
4
|
"description": "Measures how well an AI agent can navigate a codebase autonomously without human assistance",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"module": "./dist/index.mjs",
|
|
@@ -39,8 +39,8 @@
|
|
|
39
39
|
"@typescript-eslint/typescript-estree": "^8.53.0",
|
|
40
40
|
"chalk": "^5.3.0",
|
|
41
41
|
"commander": "^14.0.0",
|
|
42
|
-
"glob": "^
|
|
43
|
-
"@aiready/core": "0.9.
|
|
42
|
+
"glob": "^13.0.0",
|
|
43
|
+
"@aiready/core": "0.9.35"
|
|
44
44
|
},
|
|
45
45
|
"devDependencies": {
|
|
46
46
|
"@types/node": "^24.0.0",
|
|
@@ -28,23 +28,33 @@ describe('Agent Grounding Analyzer', () => {
|
|
|
28
28
|
describe('Deep Directories and Vague Files', () => {
|
|
29
29
|
it('should detect deep directories and vague file names', async () => {
|
|
30
30
|
// Mock files deep in the tree and with vague names
|
|
31
|
-
createTestFile(
|
|
31
|
+
createTestFile(
|
|
32
|
+
'src/components/common/utils/helpers/deep/very/deep.ts',
|
|
33
|
+
'export const x = 1;'
|
|
34
|
+
);
|
|
32
35
|
createTestFile('src/utils.ts', 'export const y = 2;');
|
|
33
36
|
|
|
34
37
|
const report = await analyzeAgentGrounding({
|
|
35
38
|
rootDir: tmpDir,
|
|
36
39
|
maxRecommendedDepth: 3,
|
|
37
|
-
additionalVagueNames: ['utils', 'helpers']
|
|
40
|
+
additionalVagueNames: ['utils', 'helpers'],
|
|
38
41
|
});
|
|
39
42
|
|
|
40
43
|
expect(report.issues.length).toBeGreaterThanOrEqual(1);
|
|
41
44
|
|
|
42
|
-
const deepIssues = report.issues.filter(
|
|
45
|
+
const deepIssues = report.issues.filter(
|
|
46
|
+
(i) =>
|
|
47
|
+
i.dimension === 'structure-clarity' && i.message.includes('exceed')
|
|
48
|
+
);
|
|
43
49
|
// The deep.ts file contributes to the aggregate depth count
|
|
44
50
|
expect(deepIssues.length).toBeGreaterThan(0);
|
|
45
51
|
|
|
46
|
-
const vagueIssues = report.issues.filter(
|
|
47
|
-
|
|
52
|
+
const vagueIssues = report.issues.filter(
|
|
53
|
+
(i) => i.dimension === 'self-documentation'
|
|
54
|
+
);
|
|
55
|
+
expect(vagueIssues.some((i) => i.message.includes('vague names'))).toBe(
|
|
56
|
+
true
|
|
57
|
+
);
|
|
48
58
|
});
|
|
49
59
|
});
|
|
50
60
|
|
|
@@ -54,7 +64,9 @@ describe('Agent Grounding Analyzer', () => {
|
|
|
54
64
|
const report = await analyzeAgentGrounding({ rootDir: tmpDir });
|
|
55
65
|
|
|
56
66
|
const issues = report.issues;
|
|
57
|
-
const readmeIssues = issues.filter(
|
|
67
|
+
const readmeIssues = issues.filter(
|
|
68
|
+
(i) => i.dimension === 'entry-point' || i.message.includes('README')
|
|
69
|
+
);
|
|
58
70
|
|
|
59
71
|
expect(readmeIssues.length).toBeGreaterThan(0);
|
|
60
72
|
});
|
|
@@ -62,16 +74,24 @@ describe('Agent Grounding Analyzer', () => {
|
|
|
62
74
|
|
|
63
75
|
describe('Untyped Exports', () => {
|
|
64
76
|
it('should detect JS files or untyped exports', async () => {
|
|
65
|
-
createTestFile(
|
|
66
|
-
|
|
77
|
+
createTestFile(
|
|
78
|
+
'src/untyped.js',
|
|
79
|
+
'export function doSomething(a, b) { return a + b; }'
|
|
80
|
+
);
|
|
81
|
+
createTestFile(
|
|
82
|
+
'src/typed.ts',
|
|
83
|
+
'export function doSomething(a: number, b: number): number { return a + b; }'
|
|
84
|
+
);
|
|
67
85
|
|
|
68
86
|
const report = await analyzeAgentGrounding({ rootDir: tmpDir });
|
|
69
87
|
|
|
70
88
|
const issues = report.issues;
|
|
71
|
-
const untypedIssues = issues.filter(i => i.dimension === 'api-clarity');
|
|
89
|
+
const untypedIssues = issues.filter((i) => i.dimension === 'api-clarity');
|
|
72
90
|
|
|
73
91
|
// The JS file untyped export contributes to the aggregate count
|
|
74
|
-
expect(
|
|
92
|
+
expect(
|
|
93
|
+
untypedIssues.some((i) => i.message.includes('lack TypeScript type'))
|
|
94
|
+
).toBe(true);
|
|
75
95
|
});
|
|
76
96
|
});
|
|
77
97
|
});
|
package/src/analyzer.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Scanner for agent-grounding dimensions.
|
|
3
|
-
*
|
|
3
|
+
*
|
|
4
4
|
* Measures 5 dimensions:
|
|
5
5
|
* 1. Structure clarity — how deep are directory trees?
|
|
6
6
|
* 2. Self-documentation — do file names reveal purpose?
|
|
@@ -9,70 +9,20 @@
|
|
|
9
9
|
* 5. Domain consistency — is the same concept named the same everywhere?
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
-
import {
|
|
13
|
-
|
|
12
|
+
import {
|
|
13
|
+
scanEntries,
|
|
14
|
+
calculateAgentGrounding,
|
|
15
|
+
VAGUE_FILE_NAMES,
|
|
16
|
+
} from '@aiready/core';
|
|
17
|
+
import { readFileSync, existsSync, statSync } from 'fs';
|
|
18
|
+
import { join, extname, basename, relative } from 'path';
|
|
14
19
|
import { parse } from '@typescript-eslint/typescript-estree';
|
|
15
20
|
import type { TSESTree } from '@typescript-eslint/types';
|
|
16
|
-
import type {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
'utils', 'helpers', 'helper', 'misc', 'common', 'shared', 'tools',
|
|
22
|
-
'util', 'lib', 'libs', 'stuff', 'functions', 'methods', 'handlers',
|
|
23
|
-
'data', 'temp', 'tmp', 'test-utils', 'test-helpers', 'mocks',
|
|
24
|
-
]);
|
|
25
|
-
|
|
26
|
-
const SUPPORTED_EXTENSIONS = new Set(['.ts', '.tsx', '.js', '.jsx']);
|
|
27
|
-
const DEFAULT_EXCLUDES = ['node_modules', 'dist', '.git', 'coverage', '.turbo', 'build'];
|
|
28
|
-
|
|
29
|
-
// ---------------------------------------------------------------------------
|
|
30
|
-
// File/dir collection
|
|
31
|
-
// ---------------------------------------------------------------------------
|
|
32
|
-
|
|
33
|
-
interface DirEntry {
|
|
34
|
-
path: string;
|
|
35
|
-
depth: number;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
function collectEntries(
|
|
39
|
-
dir: string,
|
|
40
|
-
options: AgentGroundingOptions,
|
|
41
|
-
depth = 0,
|
|
42
|
-
dirs: DirEntry[] = [],
|
|
43
|
-
files: string[] = [],
|
|
44
|
-
): { dirs: DirEntry[]; files: string[] } {
|
|
45
|
-
if (depth > (options.maxDepth ?? 20)) return { dirs, files };
|
|
46
|
-
const excludes = [...DEFAULT_EXCLUDES, ...(options.exclude ?? [])];
|
|
47
|
-
|
|
48
|
-
let entries: string[];
|
|
49
|
-
try {
|
|
50
|
-
entries = readdirSync(dir);
|
|
51
|
-
} catch {
|
|
52
|
-
return { dirs, files };
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
for (const entry of entries) {
|
|
56
|
-
if (excludes.some(ex => entry === ex || entry.includes(ex))) continue;
|
|
57
|
-
const full = join(dir, entry);
|
|
58
|
-
let stat;
|
|
59
|
-
try {
|
|
60
|
-
stat = statSync(full);
|
|
61
|
-
} catch {
|
|
62
|
-
continue;
|
|
63
|
-
}
|
|
64
|
-
if (stat.isDirectory()) {
|
|
65
|
-
dirs.push({ path: full, depth });
|
|
66
|
-
collectEntries(full, options, depth + 1, dirs, files);
|
|
67
|
-
} else if (stat.isFile() && SUPPORTED_EXTENSIONS.has(extname(full))) {
|
|
68
|
-
if (!options.include || options.include.some(p => full.includes(p))) {
|
|
69
|
-
files.push(full);
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
return { dirs, files };
|
|
75
|
-
}
|
|
21
|
+
import type {
|
|
22
|
+
AgentGroundingOptions,
|
|
23
|
+
AgentGroundingIssue,
|
|
24
|
+
AgentGroundingReport,
|
|
25
|
+
} from './types';
|
|
76
26
|
|
|
77
27
|
// ---------------------------------------------------------------------------
|
|
78
28
|
// Per-file analysis
|
|
@@ -91,7 +41,13 @@ function analyzeFile(filePath: string): FileAnalysis {
|
|
|
91
41
|
try {
|
|
92
42
|
code = readFileSync(filePath, 'utf-8');
|
|
93
43
|
} catch {
|
|
94
|
-
return {
|
|
44
|
+
return {
|
|
45
|
+
isBarrel: false,
|
|
46
|
+
exportedNames: [],
|
|
47
|
+
untypedExports: 0,
|
|
48
|
+
totalExports: 0,
|
|
49
|
+
domainTerms: [],
|
|
50
|
+
};
|
|
95
51
|
}
|
|
96
52
|
|
|
97
53
|
let ast: TSESTree.Program;
|
|
@@ -102,11 +58,17 @@ function analyzeFile(filePath: string): FileAnalysis {
|
|
|
102
58
|
loc: false,
|
|
103
59
|
});
|
|
104
60
|
} catch {
|
|
105
|
-
return {
|
|
61
|
+
return {
|
|
62
|
+
isBarrel: false,
|
|
63
|
+
exportedNames: [],
|
|
64
|
+
untypedExports: 0,
|
|
65
|
+
totalExports: 0,
|
|
66
|
+
domainTerms: [],
|
|
67
|
+
};
|
|
106
68
|
}
|
|
107
69
|
|
|
108
70
|
let isBarrel = false;
|
|
109
|
-
|
|
71
|
+
const exportedNames: string[] = [];
|
|
110
72
|
let untypedExports = 0;
|
|
111
73
|
let totalExports = 0;
|
|
112
74
|
|
|
@@ -126,7 +88,13 @@ function analyzeFile(filePath: string): FileAnalysis {
|
|
|
126
88
|
if (name) {
|
|
127
89
|
exportedNames.push(name);
|
|
128
90
|
// Split camelCase into terms
|
|
129
|
-
domainTerms.push(
|
|
91
|
+
domainTerms.push(
|
|
92
|
+
...name
|
|
93
|
+
.replace(/([A-Z])/g, ' $1')
|
|
94
|
+
.toLowerCase()
|
|
95
|
+
.split(/\s+/)
|
|
96
|
+
.filter(Boolean)
|
|
97
|
+
);
|
|
130
98
|
|
|
131
99
|
// Check if it's typed (TS function/variable with annotation)
|
|
132
100
|
const hasType =
|
|
@@ -152,7 +120,10 @@ function analyzeFile(filePath: string): FileAnalysis {
|
|
|
152
120
|
// Domain vocabulary consistency check
|
|
153
121
|
// ---------------------------------------------------------------------------
|
|
154
122
|
|
|
155
|
-
function detectInconsistentTerms(allTerms: string[]): {
|
|
123
|
+
function detectInconsistentTerms(allTerms: string[]): {
|
|
124
|
+
inconsistent: number;
|
|
125
|
+
vocabularySize: number;
|
|
126
|
+
} {
|
|
156
127
|
const termFreq = new Map<string, number>();
|
|
157
128
|
for (const term of allTerms) {
|
|
158
129
|
if (term.length >= 3) {
|
|
@@ -161,8 +132,8 @@ function detectInconsistentTerms(allTerms: string[]): { inconsistent: number; vo
|
|
|
161
132
|
}
|
|
162
133
|
// Very simplistic: terms that appear exactly once are "orphan concepts" —
|
|
163
134
|
// they may be inconsistently named variants of common terms.
|
|
164
|
-
const orphans = [...termFreq.values()].filter(count => count === 1).length;
|
|
165
|
-
const common = [...termFreq.values()].filter(count => count >= 3).length;
|
|
135
|
+
const orphans = [...termFreq.values()].filter((count) => count === 1).length;
|
|
136
|
+
const common = [...termFreq.values()].filter((count) => count >= 3).length;
|
|
166
137
|
const vocabularySize = termFreq.size;
|
|
167
138
|
// Inconsistency ratio: many orphan terms relative to common terms
|
|
168
139
|
const inconsistent = Math.max(0, orphans - common * 2);
|
|
@@ -174,19 +145,32 @@ function detectInconsistentTerms(allTerms: string[]): { inconsistent: number; vo
|
|
|
174
145
|
// ---------------------------------------------------------------------------
|
|
175
146
|
|
|
176
147
|
export async function analyzeAgentGrounding(
|
|
177
|
-
options: AgentGroundingOptions
|
|
148
|
+
options: AgentGroundingOptions
|
|
178
149
|
): Promise<AgentGroundingReport> {
|
|
179
150
|
const rootDir = options.rootDir;
|
|
180
151
|
const maxRecommendedDepth = options.maxRecommendedDepth ?? 4;
|
|
181
152
|
const readmeStaleDays = options.readmeStaleDays ?? 90;
|
|
182
153
|
|
|
183
|
-
|
|
154
|
+
// Use core scanEntries which respects .gitignore recursively
|
|
155
|
+
const { files, dirs: rawDirs } = await scanEntries({
|
|
156
|
+
...options,
|
|
157
|
+
include: options.include || ['**/*.{ts,tsx,js,jsx}'],
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
const dirs = rawDirs.map((d: string) => ({
|
|
161
|
+
path: d,
|
|
162
|
+
depth: relative(rootDir, d).split(/[/\\]/).filter(Boolean).length,
|
|
163
|
+
}));
|
|
184
164
|
|
|
185
165
|
// Structure clarity
|
|
186
|
-
const deepDirectories = dirs.filter(
|
|
166
|
+
const deepDirectories = dirs.filter(
|
|
167
|
+
(d: { path: string; depth: number }) => d.depth > maxRecommendedDepth
|
|
168
|
+
).length;
|
|
187
169
|
|
|
188
170
|
// Self-documentation — vague file names
|
|
189
|
-
const additionalVague = new Set(
|
|
171
|
+
const additionalVague = new Set(
|
|
172
|
+
(options.additionalVagueNames ?? []).map((n) => n.toLowerCase())
|
|
173
|
+
);
|
|
190
174
|
let vagueFileNames = 0;
|
|
191
175
|
for (const f of files) {
|
|
192
176
|
const base = basename(f, extname(f)).toLowerCase();
|
|
@@ -204,7 +188,9 @@ export async function analyzeAgentGrounding(
|
|
|
204
188
|
const stat = statSync(readmePath);
|
|
205
189
|
const ageDays = (Date.now() - stat.mtimeMs) / (1000 * 60 * 60 * 24);
|
|
206
190
|
readmeIsFresh = ageDays < readmeStaleDays;
|
|
207
|
-
} catch {
|
|
191
|
+
} catch {
|
|
192
|
+
/* ignore stat errors */
|
|
193
|
+
}
|
|
208
194
|
}
|
|
209
195
|
|
|
210
196
|
// File analysis
|
|
@@ -213,7 +199,15 @@ export async function analyzeAgentGrounding(
|
|
|
213
199
|
let untypedExports = 0;
|
|
214
200
|
let totalExports = 0;
|
|
215
201
|
|
|
202
|
+
let processed = 0;
|
|
216
203
|
for (const f of files) {
|
|
204
|
+
processed++;
|
|
205
|
+
options.onProgress?.(
|
|
206
|
+
processed,
|
|
207
|
+
files.length,
|
|
208
|
+
`agent-grounding: analyzing files`
|
|
209
|
+
);
|
|
210
|
+
|
|
217
211
|
const analysis = analyzeFile(f);
|
|
218
212
|
if (analysis.isBarrel) barrelExports++;
|
|
219
213
|
untypedExports += analysis.untypedExports;
|
|
@@ -222,8 +216,10 @@ export async function analyzeAgentGrounding(
|
|
|
222
216
|
}
|
|
223
217
|
|
|
224
218
|
// Domain vocabulary consistency
|
|
225
|
-
const {
|
|
226
|
-
|
|
219
|
+
const {
|
|
220
|
+
inconsistent: inconsistentDomainTerms,
|
|
221
|
+
vocabularySize: domainVocabularySize,
|
|
222
|
+
} = detectInconsistentTerms(allDomainTerms);
|
|
227
223
|
|
|
228
224
|
// Calculate grounding score using core math
|
|
229
225
|
const groundingResult = calculateAgentGrounding({
|
|
@@ -261,7 +257,8 @@ export async function analyzeAgentGrounding(
|
|
|
261
257
|
severity: 'major',
|
|
262
258
|
message: `${vagueFileNames} files use vague names (utils, helpers, misc) — an agent cannot determine their purpose from the name alone.`,
|
|
263
259
|
location: { file: rootDir, line: 0 },
|
|
264
|
-
suggestion:
|
|
260
|
+
suggestion:
|
|
261
|
+
'Rename to domain-specific names: e.g., userAuthUtils → tokenValidator.',
|
|
265
262
|
});
|
|
266
263
|
}
|
|
267
264
|
|
|
@@ -270,9 +267,11 @@ export async function analyzeAgentGrounding(
|
|
|
270
267
|
type: 'agent-navigation-failure',
|
|
271
268
|
dimension: 'entry-point',
|
|
272
269
|
severity: 'critical',
|
|
273
|
-
message:
|
|
270
|
+
message:
|
|
271
|
+
'No root README.md found — agents have no orientation document to start from.',
|
|
274
272
|
location: { file: join(rootDir, 'README.md'), line: 0 },
|
|
275
|
-
suggestion:
|
|
273
|
+
suggestion:
|
|
274
|
+
'Add a README.md explaining the project structure, entry points, and key conventions.',
|
|
276
275
|
});
|
|
277
276
|
} else if (!readmeIsFresh) {
|
|
278
277
|
issues.push({
|
|
@@ -292,7 +291,8 @@ export async function analyzeAgentGrounding(
|
|
|
292
291
|
severity: 'major',
|
|
293
292
|
message: `${untypedExports} of ${totalExports} public exports lack TypeScript type annotations — agents cannot infer the API contract.`,
|
|
294
293
|
location: { file: rootDir, line: 0 },
|
|
295
|
-
suggestion:
|
|
294
|
+
suggestion:
|
|
295
|
+
'Add explicit return type and parameter annotations to all exported functions.',
|
|
296
296
|
});
|
|
297
297
|
}
|
|
298
298
|
|
|
@@ -303,7 +303,8 @@ export async function analyzeAgentGrounding(
|
|
|
303
303
|
severity: 'major',
|
|
304
304
|
message: `${inconsistentDomainTerms} domain terms appear to be used inconsistently — agents get confused when one concept has multiple names.`,
|
|
305
305
|
location: { file: rootDir, line: 0 },
|
|
306
|
-
suggestion:
|
|
306
|
+
suggestion:
|
|
307
|
+
'Establish a domain glossary and enforce one term per concept across the codebase.',
|
|
307
308
|
});
|
|
308
309
|
}
|
|
309
310
|
|