@aiready/agent-grounding 0.1.4 → 0.1.6
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-test.log +4 -4
- package/dist/chunk-NHDH733I.mjs +336 -0
- package/dist/cli.js +81 -23
- package/dist/cli.mjs +43 -11
- package/dist/index.js +44 -14
- package/dist/index.mjs +1 -1
- package/package.json +3 -3
- package/src/__tests__/analyzer.test.ts +30 -10
- package/src/analyzer.ts +89 -27
- 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-NHDH733I.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
|
@@ -53,7 +53,14 @@ var VAGUE_FILE_NAMES = /* @__PURE__ */ new Set([
|
|
|
53
53
|
"mocks"
|
|
54
54
|
]);
|
|
55
55
|
var SUPPORTED_EXTENSIONS = /* @__PURE__ */ new Set([".ts", ".tsx", ".js", ".jsx"]);
|
|
56
|
-
var DEFAULT_EXCLUDES = [
|
|
56
|
+
var DEFAULT_EXCLUDES = [
|
|
57
|
+
"node_modules",
|
|
58
|
+
"dist",
|
|
59
|
+
".git",
|
|
60
|
+
"coverage",
|
|
61
|
+
".turbo",
|
|
62
|
+
"build"
|
|
63
|
+
];
|
|
57
64
|
function collectEntries(dir, options, depth = 0, dirs = [], files = []) {
|
|
58
65
|
if (depth > (options.maxDepth ?? 20)) return { dirs, files };
|
|
59
66
|
const excludes = [...DEFAULT_EXCLUDES, ...options.exclude ?? []];
|
|
@@ -88,7 +95,13 @@ function analyzeFile(filePath) {
|
|
|
88
95
|
try {
|
|
89
96
|
code = (0, import_fs.readFileSync)(filePath, "utf-8");
|
|
90
97
|
} catch {
|
|
91
|
-
return {
|
|
98
|
+
return {
|
|
99
|
+
isBarrel: false,
|
|
100
|
+
exportedNames: [],
|
|
101
|
+
untypedExports: 0,
|
|
102
|
+
totalExports: 0,
|
|
103
|
+
domainTerms: []
|
|
104
|
+
};
|
|
92
105
|
}
|
|
93
106
|
let ast;
|
|
94
107
|
try {
|
|
@@ -98,10 +111,16 @@ function analyzeFile(filePath) {
|
|
|
98
111
|
loc: false
|
|
99
112
|
});
|
|
100
113
|
} catch {
|
|
101
|
-
return {
|
|
114
|
+
return {
|
|
115
|
+
isBarrel: false,
|
|
116
|
+
exportedNames: [],
|
|
117
|
+
untypedExports: 0,
|
|
118
|
+
totalExports: 0,
|
|
119
|
+
domainTerms: []
|
|
120
|
+
};
|
|
102
121
|
}
|
|
103
122
|
let isBarrel = false;
|
|
104
|
-
|
|
123
|
+
const exportedNames = [];
|
|
105
124
|
let untypedExports = 0;
|
|
106
125
|
let totalExports = 0;
|
|
107
126
|
const domainTerms = [];
|
|
@@ -117,7 +136,9 @@ function analyzeFile(filePath) {
|
|
|
117
136
|
const name = decl.id?.name ?? decl.declarations?.[0]?.id?.name;
|
|
118
137
|
if (name) {
|
|
119
138
|
exportedNames.push(name);
|
|
120
|
-
domainTerms.push(
|
|
139
|
+
domainTerms.push(
|
|
140
|
+
...name.replace(/([A-Z])/g, " $1").toLowerCase().split(/\s+/).filter(Boolean)
|
|
141
|
+
);
|
|
121
142
|
const hasType = decl.returnType != null || decl.declarations?.[0]?.id?.typeAnnotation != null || decl.typeParameters != null;
|
|
122
143
|
if (!hasType) untypedExports++;
|
|
123
144
|
}
|
|
@@ -149,8 +170,12 @@ async function analyzeAgentGrounding(options) {
|
|
|
149
170
|
const maxRecommendedDepth = options.maxRecommendedDepth ?? 4;
|
|
150
171
|
const readmeStaleDays = options.readmeStaleDays ?? 90;
|
|
151
172
|
const { dirs, files } = collectEntries(rootDir, options);
|
|
152
|
-
const deepDirectories = dirs.filter(
|
|
153
|
-
|
|
173
|
+
const deepDirectories = dirs.filter(
|
|
174
|
+
(d) => d.depth > maxRecommendedDepth
|
|
175
|
+
).length;
|
|
176
|
+
const additionalVague = new Set(
|
|
177
|
+
(options.additionalVagueNames ?? []).map((n) => n.toLowerCase())
|
|
178
|
+
);
|
|
154
179
|
let vagueFileNames = 0;
|
|
155
180
|
for (const f of files) {
|
|
156
181
|
const base = (0, import_path.basename)(f, (0, import_path.extname)(f)).toLowerCase();
|
|
@@ -180,7 +205,10 @@ async function analyzeAgentGrounding(options) {
|
|
|
180
205
|
totalExports += analysis.totalExports;
|
|
181
206
|
allDomainTerms.push(...analysis.domainTerms);
|
|
182
207
|
}
|
|
183
|
-
const {
|
|
208
|
+
const {
|
|
209
|
+
inconsistent: inconsistentDomainTerms,
|
|
210
|
+
vocabularySize: domainVocabularySize
|
|
211
|
+
} = detectInconsistentTerms(allDomainTerms);
|
|
184
212
|
const groundingResult = (0, import_core.calculateAgentGrounding)({
|
|
185
213
|
deepDirectories,
|
|
186
214
|
totalDirectories: dirs.length,
|
|
@@ -282,7 +310,7 @@ async function analyzeAgentGrounding(options) {
|
|
|
282
310
|
|
|
283
311
|
// src/scoring.ts
|
|
284
312
|
function calculateGroundingScore(report) {
|
|
285
|
-
const { summary, rawData,
|
|
313
|
+
const { summary, rawData, recommendations } = report;
|
|
286
314
|
const factors = [
|
|
287
315
|
{
|
|
288
316
|
name: "Structure Clarity",
|
|
@@ -310,11 +338,13 @@ function calculateGroundingScore(report) {
|
|
|
310
338
|
description: `${rawData.inconsistentDomainTerms} inconsistent domain terms detected`
|
|
311
339
|
}
|
|
312
340
|
];
|
|
313
|
-
const recs = recommendations.map(
|
|
314
|
-
action
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
341
|
+
const recs = recommendations.map(
|
|
342
|
+
(action) => ({
|
|
343
|
+
action,
|
|
344
|
+
estimatedImpact: 6,
|
|
345
|
+
priority: summary.score < 50 ? "high" : "medium"
|
|
346
|
+
})
|
|
347
|
+
);
|
|
318
348
|
return {
|
|
319
349
|
toolName: "agent-grounding",
|
|
320
350
|
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.6",
|
|
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.33"
|
|
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?
|
|
@@ -13,18 +13,46 @@ import { readdirSync, statSync, existsSync, readFileSync } from 'fs';
|
|
|
13
13
|
import { join, extname, basename } from 'path';
|
|
14
14
|
import { parse } from '@typescript-eslint/typescript-estree';
|
|
15
15
|
import type { TSESTree } from '@typescript-eslint/types';
|
|
16
|
-
import type {
|
|
16
|
+
import type {
|
|
17
|
+
AgentGroundingOptions,
|
|
18
|
+
AgentGroundingIssue,
|
|
19
|
+
AgentGroundingReport,
|
|
20
|
+
} from './types';
|
|
17
21
|
import { calculateAgentGrounding } from '@aiready/core';
|
|
18
22
|
|
|
19
23
|
// File names that don't describe purpose — an agent can't determine what to find here
|
|
20
24
|
const VAGUE_FILE_NAMES = new Set([
|
|
21
|
-
'utils',
|
|
22
|
-
'
|
|
23
|
-
'
|
|
25
|
+
'utils',
|
|
26
|
+
'helpers',
|
|
27
|
+
'helper',
|
|
28
|
+
'misc',
|
|
29
|
+
'common',
|
|
30
|
+
'shared',
|
|
31
|
+
'tools',
|
|
32
|
+
'util',
|
|
33
|
+
'lib',
|
|
34
|
+
'libs',
|
|
35
|
+
'stuff',
|
|
36
|
+
'functions',
|
|
37
|
+
'methods',
|
|
38
|
+
'handlers',
|
|
39
|
+
'data',
|
|
40
|
+
'temp',
|
|
41
|
+
'tmp',
|
|
42
|
+
'test-utils',
|
|
43
|
+
'test-helpers',
|
|
44
|
+
'mocks',
|
|
24
45
|
]);
|
|
25
46
|
|
|
26
47
|
const SUPPORTED_EXTENSIONS = new Set(['.ts', '.tsx', '.js', '.jsx']);
|
|
27
|
-
const DEFAULT_EXCLUDES = [
|
|
48
|
+
const DEFAULT_EXCLUDES = [
|
|
49
|
+
'node_modules',
|
|
50
|
+
'dist',
|
|
51
|
+
'.git',
|
|
52
|
+
'coverage',
|
|
53
|
+
'.turbo',
|
|
54
|
+
'build',
|
|
55
|
+
];
|
|
28
56
|
|
|
29
57
|
// ---------------------------------------------------------------------------
|
|
30
58
|
// File/dir collection
|
|
@@ -40,7 +68,7 @@ function collectEntries(
|
|
|
40
68
|
options: AgentGroundingOptions,
|
|
41
69
|
depth = 0,
|
|
42
70
|
dirs: DirEntry[] = [],
|
|
43
|
-
files: string[] = []
|
|
71
|
+
files: string[] = []
|
|
44
72
|
): { dirs: DirEntry[]; files: string[] } {
|
|
45
73
|
if (depth > (options.maxDepth ?? 20)) return { dirs, files };
|
|
46
74
|
const excludes = [...DEFAULT_EXCLUDES, ...(options.exclude ?? [])];
|
|
@@ -53,7 +81,7 @@ function collectEntries(
|
|
|
53
81
|
}
|
|
54
82
|
|
|
55
83
|
for (const entry of entries) {
|
|
56
|
-
if (excludes.some(ex => entry === ex || entry.includes(ex))) continue;
|
|
84
|
+
if (excludes.some((ex) => entry === ex || entry.includes(ex))) continue;
|
|
57
85
|
const full = join(dir, entry);
|
|
58
86
|
let stat;
|
|
59
87
|
try {
|
|
@@ -65,7 +93,7 @@ function collectEntries(
|
|
|
65
93
|
dirs.push({ path: full, depth });
|
|
66
94
|
collectEntries(full, options, depth + 1, dirs, files);
|
|
67
95
|
} else if (stat.isFile() && SUPPORTED_EXTENSIONS.has(extname(full))) {
|
|
68
|
-
if (!options.include || options.include.some(p => full.includes(p))) {
|
|
96
|
+
if (!options.include || options.include.some((p) => full.includes(p))) {
|
|
69
97
|
files.push(full);
|
|
70
98
|
}
|
|
71
99
|
}
|
|
@@ -91,7 +119,13 @@ function analyzeFile(filePath: string): FileAnalysis {
|
|
|
91
119
|
try {
|
|
92
120
|
code = readFileSync(filePath, 'utf-8');
|
|
93
121
|
} catch {
|
|
94
|
-
return {
|
|
122
|
+
return {
|
|
123
|
+
isBarrel: false,
|
|
124
|
+
exportedNames: [],
|
|
125
|
+
untypedExports: 0,
|
|
126
|
+
totalExports: 0,
|
|
127
|
+
domainTerms: [],
|
|
128
|
+
};
|
|
95
129
|
}
|
|
96
130
|
|
|
97
131
|
let ast: TSESTree.Program;
|
|
@@ -102,11 +136,17 @@ function analyzeFile(filePath: string): FileAnalysis {
|
|
|
102
136
|
loc: false,
|
|
103
137
|
});
|
|
104
138
|
} catch {
|
|
105
|
-
return {
|
|
139
|
+
return {
|
|
140
|
+
isBarrel: false,
|
|
141
|
+
exportedNames: [],
|
|
142
|
+
untypedExports: 0,
|
|
143
|
+
totalExports: 0,
|
|
144
|
+
domainTerms: [],
|
|
145
|
+
};
|
|
106
146
|
}
|
|
107
147
|
|
|
108
148
|
let isBarrel = false;
|
|
109
|
-
|
|
149
|
+
const exportedNames: string[] = [];
|
|
110
150
|
let untypedExports = 0;
|
|
111
151
|
let totalExports = 0;
|
|
112
152
|
|
|
@@ -126,7 +166,13 @@ function analyzeFile(filePath: string): FileAnalysis {
|
|
|
126
166
|
if (name) {
|
|
127
167
|
exportedNames.push(name);
|
|
128
168
|
// Split camelCase into terms
|
|
129
|
-
domainTerms.push(
|
|
169
|
+
domainTerms.push(
|
|
170
|
+
...name
|
|
171
|
+
.replace(/([A-Z])/g, ' $1')
|
|
172
|
+
.toLowerCase()
|
|
173
|
+
.split(/\s+/)
|
|
174
|
+
.filter(Boolean)
|
|
175
|
+
);
|
|
130
176
|
|
|
131
177
|
// Check if it's typed (TS function/variable with annotation)
|
|
132
178
|
const hasType =
|
|
@@ -152,7 +198,10 @@ function analyzeFile(filePath: string): FileAnalysis {
|
|
|
152
198
|
// Domain vocabulary consistency check
|
|
153
199
|
// ---------------------------------------------------------------------------
|
|
154
200
|
|
|
155
|
-
function detectInconsistentTerms(allTerms: string[]): {
|
|
201
|
+
function detectInconsistentTerms(allTerms: string[]): {
|
|
202
|
+
inconsistent: number;
|
|
203
|
+
vocabularySize: number;
|
|
204
|
+
} {
|
|
156
205
|
const termFreq = new Map<string, number>();
|
|
157
206
|
for (const term of allTerms) {
|
|
158
207
|
if (term.length >= 3) {
|
|
@@ -161,8 +210,8 @@ function detectInconsistentTerms(allTerms: string[]): { inconsistent: number; vo
|
|
|
161
210
|
}
|
|
162
211
|
// Very simplistic: terms that appear exactly once are "orphan concepts" —
|
|
163
212
|
// 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;
|
|
213
|
+
const orphans = [...termFreq.values()].filter((count) => count === 1).length;
|
|
214
|
+
const common = [...termFreq.values()].filter((count) => count >= 3).length;
|
|
166
215
|
const vocabularySize = termFreq.size;
|
|
167
216
|
// Inconsistency ratio: many orphan terms relative to common terms
|
|
168
217
|
const inconsistent = Math.max(0, orphans - common * 2);
|
|
@@ -174,7 +223,7 @@ function detectInconsistentTerms(allTerms: string[]): { inconsistent: number; vo
|
|
|
174
223
|
// ---------------------------------------------------------------------------
|
|
175
224
|
|
|
176
225
|
export async function analyzeAgentGrounding(
|
|
177
|
-
options: AgentGroundingOptions
|
|
226
|
+
options: AgentGroundingOptions
|
|
178
227
|
): Promise<AgentGroundingReport> {
|
|
179
228
|
const rootDir = options.rootDir;
|
|
180
229
|
const maxRecommendedDepth = options.maxRecommendedDepth ?? 4;
|
|
@@ -183,10 +232,14 @@ export async function analyzeAgentGrounding(
|
|
|
183
232
|
const { dirs, files } = collectEntries(rootDir, options);
|
|
184
233
|
|
|
185
234
|
// Structure clarity
|
|
186
|
-
const deepDirectories = dirs.filter(
|
|
235
|
+
const deepDirectories = dirs.filter(
|
|
236
|
+
(d) => d.depth > maxRecommendedDepth
|
|
237
|
+
).length;
|
|
187
238
|
|
|
188
239
|
// Self-documentation — vague file names
|
|
189
|
-
const additionalVague = new Set(
|
|
240
|
+
const additionalVague = new Set(
|
|
241
|
+
(options.additionalVagueNames ?? []).map((n) => n.toLowerCase())
|
|
242
|
+
);
|
|
190
243
|
let vagueFileNames = 0;
|
|
191
244
|
for (const f of files) {
|
|
192
245
|
const base = basename(f, extname(f)).toLowerCase();
|
|
@@ -204,7 +257,9 @@ export async function analyzeAgentGrounding(
|
|
|
204
257
|
const stat = statSync(readmePath);
|
|
205
258
|
const ageDays = (Date.now() - stat.mtimeMs) / (1000 * 60 * 60 * 24);
|
|
206
259
|
readmeIsFresh = ageDays < readmeStaleDays;
|
|
207
|
-
} catch {
|
|
260
|
+
} catch {
|
|
261
|
+
/* ignore stat errors */
|
|
262
|
+
}
|
|
208
263
|
}
|
|
209
264
|
|
|
210
265
|
// File analysis
|
|
@@ -222,8 +277,10 @@ export async function analyzeAgentGrounding(
|
|
|
222
277
|
}
|
|
223
278
|
|
|
224
279
|
// Domain vocabulary consistency
|
|
225
|
-
const {
|
|
226
|
-
|
|
280
|
+
const {
|
|
281
|
+
inconsistent: inconsistentDomainTerms,
|
|
282
|
+
vocabularySize: domainVocabularySize,
|
|
283
|
+
} = detectInconsistentTerms(allDomainTerms);
|
|
227
284
|
|
|
228
285
|
// Calculate grounding score using core math
|
|
229
286
|
const groundingResult = calculateAgentGrounding({
|
|
@@ -261,7 +318,8 @@ export async function analyzeAgentGrounding(
|
|
|
261
318
|
severity: 'major',
|
|
262
319
|
message: `${vagueFileNames} files use vague names (utils, helpers, misc) — an agent cannot determine their purpose from the name alone.`,
|
|
263
320
|
location: { file: rootDir, line: 0 },
|
|
264
|
-
suggestion:
|
|
321
|
+
suggestion:
|
|
322
|
+
'Rename to domain-specific names: e.g., userAuthUtils → tokenValidator.',
|
|
265
323
|
});
|
|
266
324
|
}
|
|
267
325
|
|
|
@@ -270,9 +328,11 @@ export async function analyzeAgentGrounding(
|
|
|
270
328
|
type: 'agent-navigation-failure',
|
|
271
329
|
dimension: 'entry-point',
|
|
272
330
|
severity: 'critical',
|
|
273
|
-
message:
|
|
331
|
+
message:
|
|
332
|
+
'No root README.md found — agents have no orientation document to start from.',
|
|
274
333
|
location: { file: join(rootDir, 'README.md'), line: 0 },
|
|
275
|
-
suggestion:
|
|
334
|
+
suggestion:
|
|
335
|
+
'Add a README.md explaining the project structure, entry points, and key conventions.',
|
|
276
336
|
});
|
|
277
337
|
} else if (!readmeIsFresh) {
|
|
278
338
|
issues.push({
|
|
@@ -292,7 +352,8 @@ export async function analyzeAgentGrounding(
|
|
|
292
352
|
severity: 'major',
|
|
293
353
|
message: `${untypedExports} of ${totalExports} public exports lack TypeScript type annotations — agents cannot infer the API contract.`,
|
|
294
354
|
location: { file: rootDir, line: 0 },
|
|
295
|
-
suggestion:
|
|
355
|
+
suggestion:
|
|
356
|
+
'Add explicit return type and parameter annotations to all exported functions.',
|
|
296
357
|
});
|
|
297
358
|
}
|
|
298
359
|
|
|
@@ -303,7 +364,8 @@ export async function analyzeAgentGrounding(
|
|
|
303
364
|
severity: 'major',
|
|
304
365
|
message: `${inconsistentDomainTerms} domain terms appear to be used inconsistently — agents get confused when one concept has multiple names.`,
|
|
305
366
|
location: { file: rootDir, line: 0 },
|
|
306
|
-
suggestion:
|
|
367
|
+
suggestion:
|
|
368
|
+
'Establish a domain glossary and enforce one term per concept across the codebase.',
|
|
307
369
|
});
|
|
308
370
|
}
|
|
309
371
|
|