@aiready/agent-grounding 0.1.6 → 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.
@@ -1,6 +1,6 @@
1
1
 
2
2
  
3
- > @aiready/agent-grounding@0.1.6 build /Users/pengcao/projects/aiready/packages/agent-grounding
3
+ > @aiready/agent-grounding@0.1.8 build /Users/pengcao/projects/aiready/packages/agent-grounding
4
4
  > tsup src/index.ts src/cli.ts --format cjs,esm --dts
5
5
 
6
6
  CLI Building entry: src/cli.ts, src/index.ts
@@ -9,15 +9,15 @@
9
9
  CLI Target: es2020
10
10
  CJS Build start
11
11
  ESM Build start
12
- ESM dist/chunk-NHDH733I.mjs 10.51 KB
12
+ CJS dist/cli.js 16.22 KB
13
+ CJS dist/index.js 10.75 KB
14
+ CJS ⚡️ Build success in 267ms
13
15
  ESM dist/index.mjs 154.00 B
14
16
  ESM dist/cli.mjs 5.16 KB
15
- ESM ⚡️ Build success in 39ms
16
- CJS dist/index.js 11.76 KB
17
- CJS dist/cli.js 17.24 KB
18
- CJS ⚡️ Build success in 39ms
17
+ ESM dist/chunk-NXIMJNCK.mjs 9.55 KB
18
+ ESM ⚡️ Build success in 266ms
19
19
  DTS Build start
20
- DTS ⚡️ Build success in 4273ms
20
+ DTS ⚡️ Build success in 10475ms
21
21
  DTS dist/cli.d.ts 20.00 B
22
22
  DTS dist/index.d.ts 2.34 KB
23
23
  DTS dist/cli.d.mts 20.00 B
@@ -0,0 +1,5 @@
1
+
2
+ 
3
+ > @aiready/agent-grounding@0.1.6 lint /Users/pengcao/projects/aiready/packages/agent-grounding
4
+ > eslint src
5
+
@@ -1,16 +1,17 @@
1
1
 
2
2
  
3
- > @aiready/agent-grounding@0.1.6 test /Users/pengcao/projects/aiready/packages/agent-grounding
3
+ > @aiready/agent-grounding@0.1.8 test /Users/pengcao/projects/aiready/packages/agent-grounding
4
4
  > vitest run
5
5
 
6
6
  [?25l
7
7
   RUN  v4.0.18 /Users/pengcao/projects/aiready/packages/agent-grounding
8
8
 
9
- ✓ src/__tests__/analyzer.test.ts (3 tests) 16ms
9
+ ✓ src/__tests__/analyzer.test.ts (3 tests) 719ms
10
+ ✓ should detect deep directories and vague file names  336ms
10
11
 
11
12
   Test Files  1 passed (1)
12
13
   Tests  3 passed (3)
13
-  Start at  22:19:17
14
-  Duration  829ms (transform 144ms, setup 0ms, import 656ms, tests 16ms, environment 0ms)
14
+  Start at  00:56:24
15
+  Duration  5.49s (transform 1.39s, setup 0ms, import 3.93s, tests 719ms, environment 0ms)
15
16
 
16
17
  [?25h
@@ -0,0 +1,294 @@
1
+ // src/analyzer.ts
2
+ import {
3
+ scanEntries,
4
+ calculateAgentGrounding,
5
+ VAGUE_FILE_NAMES
6
+ } from "@aiready/core";
7
+ import { readFileSync, existsSync, statSync } from "fs";
8
+ import { join, extname, basename, relative } from "path";
9
+ import { parse } from "@typescript-eslint/typescript-estree";
10
+ function analyzeFile(filePath) {
11
+ let code;
12
+ try {
13
+ code = readFileSync(filePath, "utf-8");
14
+ } catch {
15
+ return {
16
+ isBarrel: false,
17
+ exportedNames: [],
18
+ untypedExports: 0,
19
+ totalExports: 0,
20
+ domainTerms: []
21
+ };
22
+ }
23
+ let ast;
24
+ try {
25
+ ast = parse(code, {
26
+ jsx: filePath.endsWith(".tsx") || filePath.endsWith(".jsx"),
27
+ range: false,
28
+ loc: false
29
+ });
30
+ } catch {
31
+ return {
32
+ isBarrel: false,
33
+ exportedNames: [],
34
+ untypedExports: 0,
35
+ totalExports: 0,
36
+ domainTerms: []
37
+ };
38
+ }
39
+ let isBarrel = false;
40
+ const exportedNames = [];
41
+ let untypedExports = 0;
42
+ let totalExports = 0;
43
+ const domainTerms = [];
44
+ for (const node of ast.body) {
45
+ if (node.type === "ExportAllDeclaration") {
46
+ isBarrel = true;
47
+ continue;
48
+ }
49
+ if (node.type === "ExportNamedDeclaration") {
50
+ totalExports++;
51
+ const decl = node.declaration;
52
+ if (decl) {
53
+ const name = decl.id?.name ?? decl.declarations?.[0]?.id?.name;
54
+ if (name) {
55
+ exportedNames.push(name);
56
+ domainTerms.push(
57
+ ...name.replace(/([A-Z])/g, " $1").toLowerCase().split(/\s+/).filter(Boolean)
58
+ );
59
+ const hasType = decl.returnType != null || decl.declarations?.[0]?.id?.typeAnnotation != null || decl.typeParameters != null;
60
+ if (!hasType) untypedExports++;
61
+ }
62
+ } else if (node.specifiers && node.specifiers.length > 0) {
63
+ isBarrel = true;
64
+ }
65
+ }
66
+ if (node.type === "ExportDefaultDeclaration") {
67
+ totalExports++;
68
+ }
69
+ }
70
+ return { isBarrel, exportedNames, untypedExports, totalExports, domainTerms };
71
+ }
72
+ function detectInconsistentTerms(allTerms) {
73
+ const termFreq = /* @__PURE__ */ new Map();
74
+ for (const term of allTerms) {
75
+ if (term.length >= 3) {
76
+ termFreq.set(term, (termFreq.get(term) ?? 0) + 1);
77
+ }
78
+ }
79
+ const orphans = [...termFreq.values()].filter((count) => count === 1).length;
80
+ const common = [...termFreq.values()].filter((count) => count >= 3).length;
81
+ const vocabularySize = termFreq.size;
82
+ const inconsistent = Math.max(0, orphans - common * 2);
83
+ return { inconsistent, vocabularySize };
84
+ }
85
+ async function analyzeAgentGrounding(options) {
86
+ const rootDir = options.rootDir;
87
+ const maxRecommendedDepth = options.maxRecommendedDepth ?? 4;
88
+ const readmeStaleDays = options.readmeStaleDays ?? 90;
89
+ const { files, dirs: rawDirs } = await scanEntries({
90
+ ...options,
91
+ include: options.include || ["**/*.{ts,tsx,js,jsx}"]
92
+ });
93
+ const dirs = rawDirs.map((d) => ({
94
+ path: d,
95
+ depth: relative(rootDir, d).split(/[/\\]/).filter(Boolean).length
96
+ }));
97
+ const deepDirectories = dirs.filter(
98
+ (d) => d.depth > maxRecommendedDepth
99
+ ).length;
100
+ const additionalVague = new Set(
101
+ (options.additionalVagueNames ?? []).map((n) => n.toLowerCase())
102
+ );
103
+ let vagueFileNames = 0;
104
+ for (const f of files) {
105
+ const base = basename(f, extname(f)).toLowerCase();
106
+ if (VAGUE_FILE_NAMES.has(base) || additionalVague.has(base)) {
107
+ vagueFileNames++;
108
+ }
109
+ }
110
+ const readmePath = join(rootDir, "README.md");
111
+ const hasRootReadme = existsSync(readmePath);
112
+ let readmeIsFresh = false;
113
+ if (hasRootReadme) {
114
+ try {
115
+ const stat = statSync(readmePath);
116
+ const ageDays = (Date.now() - stat.mtimeMs) / (1e3 * 60 * 60 * 24);
117
+ readmeIsFresh = ageDays < readmeStaleDays;
118
+ } catch {
119
+ }
120
+ }
121
+ const allDomainTerms = [];
122
+ let barrelExports = 0;
123
+ let untypedExports = 0;
124
+ let totalExports = 0;
125
+ let processed = 0;
126
+ for (const f of files) {
127
+ processed++;
128
+ options.onProgress?.(
129
+ processed,
130
+ files.length,
131
+ `agent-grounding: analyzing files`
132
+ );
133
+ const analysis = analyzeFile(f);
134
+ if (analysis.isBarrel) barrelExports++;
135
+ untypedExports += analysis.untypedExports;
136
+ totalExports += analysis.totalExports;
137
+ allDomainTerms.push(...analysis.domainTerms);
138
+ }
139
+ const {
140
+ inconsistent: inconsistentDomainTerms,
141
+ vocabularySize: domainVocabularySize
142
+ } = detectInconsistentTerms(allDomainTerms);
143
+ const groundingResult = calculateAgentGrounding({
144
+ deepDirectories,
145
+ totalDirectories: dirs.length,
146
+ vagueFileNames,
147
+ totalFiles: files.length,
148
+ hasRootReadme,
149
+ readmeIsFresh,
150
+ barrelExports,
151
+ untypedExports,
152
+ totalExports: Math.max(1, totalExports),
153
+ inconsistentDomainTerms,
154
+ domainVocabularySize: Math.max(1, domainVocabularySize)
155
+ });
156
+ const issues = [];
157
+ if (groundingResult.dimensions.structureClarityScore < 70) {
158
+ issues.push({
159
+ type: "agent-navigation-failure",
160
+ dimension: "structure-clarity",
161
+ severity: "major",
162
+ message: `${deepDirectories} directories exceed recommended depth of ${maxRecommendedDepth} \u2014 agents struggle to navigate deep trees.`,
163
+ location: { file: rootDir, line: 0 },
164
+ suggestion: `Flatten nested directories to ${maxRecommendedDepth} levels or fewer.`
165
+ });
166
+ }
167
+ if (groundingResult.dimensions.selfDocumentationScore < 70) {
168
+ issues.push({
169
+ type: "agent-navigation-failure",
170
+ dimension: "self-documentation",
171
+ severity: "major",
172
+ message: `${vagueFileNames} files use vague names (utils, helpers, misc) \u2014 an agent cannot determine their purpose from the name alone.`,
173
+ location: { file: rootDir, line: 0 },
174
+ suggestion: "Rename to domain-specific names: e.g., userAuthUtils \u2192 tokenValidator."
175
+ });
176
+ }
177
+ if (!hasRootReadme) {
178
+ issues.push({
179
+ type: "agent-navigation-failure",
180
+ dimension: "entry-point",
181
+ severity: "critical",
182
+ message: "No root README.md found \u2014 agents have no orientation document to start from.",
183
+ location: { file: join(rootDir, "README.md"), line: 0 },
184
+ suggestion: "Add a README.md explaining the project structure, entry points, and key conventions."
185
+ });
186
+ } else if (!readmeIsFresh) {
187
+ issues.push({
188
+ type: "agent-navigation-failure",
189
+ dimension: "entry-point",
190
+ severity: "minor",
191
+ message: `README.md is stale (>${readmeStaleDays} days without updates) \u2014 agents may be misled by outdated context.`,
192
+ location: { file: readmePath, line: 0 },
193
+ suggestion: "Update README.md to reflect the current codebase structure."
194
+ });
195
+ }
196
+ if (groundingResult.dimensions.apiClarityScore < 70) {
197
+ issues.push({
198
+ type: "agent-navigation-failure",
199
+ dimension: "api-clarity",
200
+ severity: "major",
201
+ message: `${untypedExports} of ${totalExports} public exports lack TypeScript type annotations \u2014 agents cannot infer the API contract.`,
202
+ location: { file: rootDir, line: 0 },
203
+ suggestion: "Add explicit return type and parameter annotations to all exported functions."
204
+ });
205
+ }
206
+ if (groundingResult.dimensions.domainConsistencyScore < 70) {
207
+ issues.push({
208
+ type: "agent-navigation-failure",
209
+ dimension: "domain-consistency",
210
+ severity: "major",
211
+ message: `${inconsistentDomainTerms} domain terms appear to be used inconsistently \u2014 agents get confused when one concept has multiple names.`,
212
+ location: { file: rootDir, line: 0 },
213
+ suggestion: "Establish a domain glossary and enforce one term per concept across the codebase."
214
+ });
215
+ }
216
+ return {
217
+ summary: {
218
+ filesAnalyzed: files.length,
219
+ directoriesAnalyzed: dirs.length,
220
+ score: groundingResult.score,
221
+ rating: groundingResult.rating,
222
+ dimensions: groundingResult.dimensions
223
+ },
224
+ issues,
225
+ rawData: {
226
+ deepDirectories,
227
+ totalDirectories: dirs.length,
228
+ vagueFileNames,
229
+ totalFiles: files.length,
230
+ hasRootReadme,
231
+ readmeIsFresh,
232
+ barrelExports,
233
+ untypedExports,
234
+ totalExports,
235
+ inconsistentDomainTerms,
236
+ domainVocabularySize
237
+ },
238
+ recommendations: groundingResult.recommendations
239
+ };
240
+ }
241
+
242
+ // src/scoring.ts
243
+ function calculateGroundingScore(report) {
244
+ const { summary, rawData, recommendations } = report;
245
+ const factors = [
246
+ {
247
+ name: "Structure Clarity",
248
+ impact: Math.round(summary.dimensions.structureClarityScore - 50),
249
+ description: `${rawData.deepDirectories} of ${rawData.totalDirectories} dirs exceed recommended depth`
250
+ },
251
+ {
252
+ name: "Self-Documentation",
253
+ impact: Math.round(summary.dimensions.selfDocumentationScore - 50),
254
+ description: `${rawData.vagueFileNames} of ${rawData.totalFiles} files have vague names`
255
+ },
256
+ {
257
+ name: "Entry Points",
258
+ impact: Math.round(summary.dimensions.entryPointScore - 50),
259
+ description: rawData.hasRootReadme ? rawData.readmeIsFresh ? "README present and fresh" : "README present but stale" : "No root README"
260
+ },
261
+ {
262
+ name: "API Clarity",
263
+ impact: Math.round(summary.dimensions.apiClarityScore - 50),
264
+ description: `${rawData.untypedExports} of ${rawData.totalExports} exports lack type annotations`
265
+ },
266
+ {
267
+ name: "Domain Consistency",
268
+ impact: Math.round(summary.dimensions.domainConsistencyScore - 50),
269
+ description: `${rawData.inconsistentDomainTerms} inconsistent domain terms detected`
270
+ }
271
+ ];
272
+ const recs = recommendations.map(
273
+ (action) => ({
274
+ action,
275
+ estimatedImpact: 6,
276
+ priority: summary.score < 50 ? "high" : "medium"
277
+ })
278
+ );
279
+ return {
280
+ toolName: "agent-grounding",
281
+ score: summary.score,
282
+ rawMetrics: {
283
+ ...rawData,
284
+ rating: summary.rating
285
+ },
286
+ factors,
287
+ recommendations: recs
288
+ };
289
+ }
290
+
291
+ export {
292
+ analyzeAgentGrounding,
293
+ calculateGroundingScore
294
+ };
@@ -0,0 +1,339 @@
1
+ // src/analyzer.ts
2
+ import { readdirSync, statSync, existsSync, readFileSync } from "fs";
3
+ import { join, extname, basename } from "path";
4
+ import { parse } from "@typescript-eslint/typescript-estree";
5
+ import { calculateAgentGrounding } from "@aiready/core";
6
+ var VAGUE_FILE_NAMES = /* @__PURE__ */ new Set([
7
+ "utils",
8
+ "helpers",
9
+ "helper",
10
+ "misc",
11
+ "common",
12
+ "shared",
13
+ "tools",
14
+ "util",
15
+ "lib",
16
+ "libs",
17
+ "stuff",
18
+ "functions",
19
+ "methods",
20
+ "handlers",
21
+ "data",
22
+ "temp",
23
+ "tmp",
24
+ "test-utils",
25
+ "test-helpers",
26
+ "mocks"
27
+ ]);
28
+ var SUPPORTED_EXTENSIONS = /* @__PURE__ */ new Set([".ts", ".tsx", ".js", ".jsx"]);
29
+ var DEFAULT_EXCLUDES = [
30
+ "node_modules",
31
+ "dist",
32
+ ".git",
33
+ "coverage",
34
+ ".turbo",
35
+ "build"
36
+ ];
37
+ function collectEntries(dir, options, depth = 0, dirs = [], files = []) {
38
+ if (depth > (options.maxDepth ?? 20)) return { dirs, files };
39
+ const excludes = [...DEFAULT_EXCLUDES, ...options.exclude ?? []];
40
+ let entries;
41
+ try {
42
+ entries = readdirSync(dir);
43
+ } catch {
44
+ return { dirs, files };
45
+ }
46
+ for (const entry of entries) {
47
+ if (excludes.some((ex) => entry === ex || entry.includes(ex))) continue;
48
+ const full = join(dir, entry);
49
+ let stat;
50
+ try {
51
+ stat = statSync(full);
52
+ } catch {
53
+ continue;
54
+ }
55
+ if (stat.isDirectory()) {
56
+ dirs.push({ path: full, depth });
57
+ collectEntries(full, options, depth + 1, dirs, files);
58
+ } else if (stat.isFile() && SUPPORTED_EXTENSIONS.has(extname(full))) {
59
+ if (!options.include || options.include.some((p) => full.includes(p))) {
60
+ files.push(full);
61
+ }
62
+ }
63
+ }
64
+ return { dirs, files };
65
+ }
66
+ function analyzeFile(filePath) {
67
+ let code;
68
+ try {
69
+ code = readFileSync(filePath, "utf-8");
70
+ } catch {
71
+ return {
72
+ isBarrel: false,
73
+ exportedNames: [],
74
+ untypedExports: 0,
75
+ totalExports: 0,
76
+ domainTerms: []
77
+ };
78
+ }
79
+ let ast;
80
+ try {
81
+ ast = parse(code, {
82
+ jsx: filePath.endsWith(".tsx") || filePath.endsWith(".jsx"),
83
+ range: false,
84
+ loc: false
85
+ });
86
+ } catch {
87
+ return {
88
+ isBarrel: false,
89
+ exportedNames: [],
90
+ untypedExports: 0,
91
+ totalExports: 0,
92
+ domainTerms: []
93
+ };
94
+ }
95
+ let isBarrel = false;
96
+ const exportedNames = [];
97
+ let untypedExports = 0;
98
+ let totalExports = 0;
99
+ const domainTerms = [];
100
+ for (const node of ast.body) {
101
+ if (node.type === "ExportAllDeclaration") {
102
+ isBarrel = true;
103
+ continue;
104
+ }
105
+ if (node.type === "ExportNamedDeclaration") {
106
+ totalExports++;
107
+ const decl = node.declaration;
108
+ if (decl) {
109
+ const name = decl.id?.name ?? decl.declarations?.[0]?.id?.name;
110
+ if (name) {
111
+ exportedNames.push(name);
112
+ domainTerms.push(
113
+ ...name.replace(/([A-Z])/g, " $1").toLowerCase().split(/\s+/).filter(Boolean)
114
+ );
115
+ const hasType = decl.returnType != null || decl.declarations?.[0]?.id?.typeAnnotation != null || decl.typeParameters != null;
116
+ if (!hasType) untypedExports++;
117
+ }
118
+ } else if (node.specifiers && node.specifiers.length > 0) {
119
+ isBarrel = true;
120
+ }
121
+ }
122
+ if (node.type === "ExportDefaultDeclaration") {
123
+ totalExports++;
124
+ }
125
+ }
126
+ return { isBarrel, exportedNames, untypedExports, totalExports, domainTerms };
127
+ }
128
+ function detectInconsistentTerms(allTerms) {
129
+ const termFreq = /* @__PURE__ */ new Map();
130
+ for (const term of allTerms) {
131
+ if (term.length >= 3) {
132
+ termFreq.set(term, (termFreq.get(term) ?? 0) + 1);
133
+ }
134
+ }
135
+ const orphans = [...termFreq.values()].filter((count) => count === 1).length;
136
+ const common = [...termFreq.values()].filter((count) => count >= 3).length;
137
+ const vocabularySize = termFreq.size;
138
+ const inconsistent = Math.max(0, orphans - common * 2);
139
+ return { inconsistent, vocabularySize };
140
+ }
141
+ async function analyzeAgentGrounding(options) {
142
+ const rootDir = options.rootDir;
143
+ const maxRecommendedDepth = options.maxRecommendedDepth ?? 4;
144
+ const readmeStaleDays = options.readmeStaleDays ?? 90;
145
+ const { dirs, files } = collectEntries(rootDir, options);
146
+ const deepDirectories = dirs.filter(
147
+ (d) => d.depth > maxRecommendedDepth
148
+ ).length;
149
+ const additionalVague = new Set(
150
+ (options.additionalVagueNames ?? []).map((n) => n.toLowerCase())
151
+ );
152
+ let vagueFileNames = 0;
153
+ for (const f of files) {
154
+ const base = basename(f, extname(f)).toLowerCase();
155
+ if (VAGUE_FILE_NAMES.has(base) || additionalVague.has(base)) {
156
+ vagueFileNames++;
157
+ }
158
+ }
159
+ const readmePath = join(rootDir, "README.md");
160
+ const hasRootReadme = existsSync(readmePath);
161
+ let readmeIsFresh = false;
162
+ if (hasRootReadme) {
163
+ try {
164
+ const stat = statSync(readmePath);
165
+ const ageDays = (Date.now() - stat.mtimeMs) / (1e3 * 60 * 60 * 24);
166
+ readmeIsFresh = ageDays < readmeStaleDays;
167
+ } catch {
168
+ }
169
+ }
170
+ const allDomainTerms = [];
171
+ let barrelExports = 0;
172
+ let untypedExports = 0;
173
+ let totalExports = 0;
174
+ let processed = 0;
175
+ for (const f of files) {
176
+ processed++;
177
+ options.onProgress?.(processed, files.length, `agent-grounding: analyzing ${f.substring(rootDir.length + 1)}`);
178
+ const analysis = analyzeFile(f);
179
+ if (analysis.isBarrel) barrelExports++;
180
+ untypedExports += analysis.untypedExports;
181
+ totalExports += analysis.totalExports;
182
+ allDomainTerms.push(...analysis.domainTerms);
183
+ }
184
+ const {
185
+ inconsistent: inconsistentDomainTerms,
186
+ vocabularySize: domainVocabularySize
187
+ } = detectInconsistentTerms(allDomainTerms);
188
+ const groundingResult = calculateAgentGrounding({
189
+ deepDirectories,
190
+ totalDirectories: dirs.length,
191
+ vagueFileNames,
192
+ totalFiles: files.length,
193
+ hasRootReadme,
194
+ readmeIsFresh,
195
+ barrelExports,
196
+ untypedExports,
197
+ totalExports: Math.max(1, totalExports),
198
+ inconsistentDomainTerms,
199
+ domainVocabularySize: Math.max(1, domainVocabularySize)
200
+ });
201
+ const issues = [];
202
+ if (groundingResult.dimensions.structureClarityScore < 70) {
203
+ issues.push({
204
+ type: "agent-navigation-failure",
205
+ dimension: "structure-clarity",
206
+ severity: "major",
207
+ message: `${deepDirectories} directories exceed recommended depth of ${maxRecommendedDepth} \u2014 agents struggle to navigate deep trees.`,
208
+ location: { file: rootDir, line: 0 },
209
+ suggestion: `Flatten nested directories to ${maxRecommendedDepth} levels or fewer.`
210
+ });
211
+ }
212
+ if (groundingResult.dimensions.selfDocumentationScore < 70) {
213
+ issues.push({
214
+ type: "agent-navigation-failure",
215
+ dimension: "self-documentation",
216
+ severity: "major",
217
+ message: `${vagueFileNames} files use vague names (utils, helpers, misc) \u2014 an agent cannot determine their purpose from the name alone.`,
218
+ location: { file: rootDir, line: 0 },
219
+ suggestion: "Rename to domain-specific names: e.g., userAuthUtils \u2192 tokenValidator."
220
+ });
221
+ }
222
+ if (!hasRootReadme) {
223
+ issues.push({
224
+ type: "agent-navigation-failure",
225
+ dimension: "entry-point",
226
+ severity: "critical",
227
+ message: "No root README.md found \u2014 agents have no orientation document to start from.",
228
+ location: { file: join(rootDir, "README.md"), line: 0 },
229
+ suggestion: "Add a README.md explaining the project structure, entry points, and key conventions."
230
+ });
231
+ } else if (!readmeIsFresh) {
232
+ issues.push({
233
+ type: "agent-navigation-failure",
234
+ dimension: "entry-point",
235
+ severity: "minor",
236
+ message: `README.md is stale (>${readmeStaleDays} days without updates) \u2014 agents may be misled by outdated context.`,
237
+ location: { file: readmePath, line: 0 },
238
+ suggestion: "Update README.md to reflect the current codebase structure."
239
+ });
240
+ }
241
+ if (groundingResult.dimensions.apiClarityScore < 70) {
242
+ issues.push({
243
+ type: "agent-navigation-failure",
244
+ dimension: "api-clarity",
245
+ severity: "major",
246
+ message: `${untypedExports} of ${totalExports} public exports lack TypeScript type annotations \u2014 agents cannot infer the API contract.`,
247
+ location: { file: rootDir, line: 0 },
248
+ suggestion: "Add explicit return type and parameter annotations to all exported functions."
249
+ });
250
+ }
251
+ if (groundingResult.dimensions.domainConsistencyScore < 70) {
252
+ issues.push({
253
+ type: "agent-navigation-failure",
254
+ dimension: "domain-consistency",
255
+ severity: "major",
256
+ message: `${inconsistentDomainTerms} domain terms appear to be used inconsistently \u2014 agents get confused when one concept has multiple names.`,
257
+ location: { file: rootDir, line: 0 },
258
+ suggestion: "Establish a domain glossary and enforce one term per concept across the codebase."
259
+ });
260
+ }
261
+ return {
262
+ summary: {
263
+ filesAnalyzed: files.length,
264
+ directoriesAnalyzed: dirs.length,
265
+ score: groundingResult.score,
266
+ rating: groundingResult.rating,
267
+ dimensions: groundingResult.dimensions
268
+ },
269
+ issues,
270
+ rawData: {
271
+ deepDirectories,
272
+ totalDirectories: dirs.length,
273
+ vagueFileNames,
274
+ totalFiles: files.length,
275
+ hasRootReadme,
276
+ readmeIsFresh,
277
+ barrelExports,
278
+ untypedExports,
279
+ totalExports,
280
+ inconsistentDomainTerms,
281
+ domainVocabularySize
282
+ },
283
+ recommendations: groundingResult.recommendations
284
+ };
285
+ }
286
+
287
+ // src/scoring.ts
288
+ function calculateGroundingScore(report) {
289
+ const { summary, rawData, recommendations } = report;
290
+ const factors = [
291
+ {
292
+ name: "Structure Clarity",
293
+ impact: Math.round(summary.dimensions.structureClarityScore - 50),
294
+ description: `${rawData.deepDirectories} of ${rawData.totalDirectories} dirs exceed recommended depth`
295
+ },
296
+ {
297
+ name: "Self-Documentation",
298
+ impact: Math.round(summary.dimensions.selfDocumentationScore - 50),
299
+ description: `${rawData.vagueFileNames} of ${rawData.totalFiles} files have vague names`
300
+ },
301
+ {
302
+ name: "Entry Points",
303
+ impact: Math.round(summary.dimensions.entryPointScore - 50),
304
+ description: rawData.hasRootReadme ? rawData.readmeIsFresh ? "README present and fresh" : "README present but stale" : "No root README"
305
+ },
306
+ {
307
+ name: "API Clarity",
308
+ impact: Math.round(summary.dimensions.apiClarityScore - 50),
309
+ description: `${rawData.untypedExports} of ${rawData.totalExports} exports lack type annotations`
310
+ },
311
+ {
312
+ name: "Domain Consistency",
313
+ impact: Math.round(summary.dimensions.domainConsistencyScore - 50),
314
+ description: `${rawData.inconsistentDomainTerms} inconsistent domain terms detected`
315
+ }
316
+ ];
317
+ const recs = recommendations.map(
318
+ (action) => ({
319
+ action,
320
+ estimatedImpact: 6,
321
+ priority: summary.score < 50 ? "high" : "medium"
322
+ })
323
+ );
324
+ return {
325
+ toolName: "agent-grounding",
326
+ score: summary.score,
327
+ rawMetrics: {
328
+ ...rawData,
329
+ rating: summary.rating
330
+ },
331
+ factors,
332
+ recommendations: recs
333
+ };
334
+ }
335
+
336
+ export {
337
+ analyzeAgentGrounding,
338
+ calculateGroundingScore
339
+ };
package/dist/cli.js CHANGED
@@ -27,70 +27,10 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
27
27
  var import_commander = require("commander");
28
28
 
29
29
  // src/analyzer.ts
30
+ var import_core = require("@aiready/core");
30
31
  var import_fs = require("fs");
31
32
  var import_path = require("path");
32
33
  var import_typescript_estree = require("@typescript-eslint/typescript-estree");
33
- var import_core = require("@aiready/core");
34
- var VAGUE_FILE_NAMES = /* @__PURE__ */ new Set([
35
- "utils",
36
- "helpers",
37
- "helper",
38
- "misc",
39
- "common",
40
- "shared",
41
- "tools",
42
- "util",
43
- "lib",
44
- "libs",
45
- "stuff",
46
- "functions",
47
- "methods",
48
- "handlers",
49
- "data",
50
- "temp",
51
- "tmp",
52
- "test-utils",
53
- "test-helpers",
54
- "mocks"
55
- ]);
56
- var SUPPORTED_EXTENSIONS = /* @__PURE__ */ new Set([".ts", ".tsx", ".js", ".jsx"]);
57
- var DEFAULT_EXCLUDES = [
58
- "node_modules",
59
- "dist",
60
- ".git",
61
- "coverage",
62
- ".turbo",
63
- "build"
64
- ];
65
- function collectEntries(dir, options, depth = 0, dirs = [], files = []) {
66
- if (depth > (options.maxDepth ?? 20)) return { dirs, files };
67
- const excludes = [...DEFAULT_EXCLUDES, ...options.exclude ?? []];
68
- let entries;
69
- try {
70
- entries = (0, import_fs.readdirSync)(dir);
71
- } catch {
72
- return { dirs, files };
73
- }
74
- for (const entry of entries) {
75
- if (excludes.some((ex) => entry === ex || entry.includes(ex))) continue;
76
- const full = (0, import_path.join)(dir, entry);
77
- let stat;
78
- try {
79
- stat = (0, import_fs.statSync)(full);
80
- } catch {
81
- continue;
82
- }
83
- if (stat.isDirectory()) {
84
- dirs.push({ path: full, depth });
85
- collectEntries(full, options, depth + 1, dirs, files);
86
- } else if (stat.isFile() && SUPPORTED_EXTENSIONS.has((0, import_path.extname)(full))) {
87
- if (!options.include || options.include.some((p) => full.includes(p))) {
88
- files.push(full);
89
- }
90
- }
91
- }
92
- return { dirs, files };
93
- }
94
34
  function analyzeFile(filePath) {
95
35
  let code;
96
36
  try {
@@ -170,7 +110,14 @@ async function analyzeAgentGrounding(options) {
170
110
  const rootDir = options.rootDir;
171
111
  const maxRecommendedDepth = options.maxRecommendedDepth ?? 4;
172
112
  const readmeStaleDays = options.readmeStaleDays ?? 90;
173
- const { dirs, files } = collectEntries(rootDir, options);
113
+ const { files, dirs: rawDirs } = await (0, import_core.scanEntries)({
114
+ ...options,
115
+ include: options.include || ["**/*.{ts,tsx,js,jsx}"]
116
+ });
117
+ const dirs = rawDirs.map((d) => ({
118
+ path: d,
119
+ depth: (0, import_path.relative)(rootDir, d).split(/[/\\]/).filter(Boolean).length
120
+ }));
174
121
  const deepDirectories = dirs.filter(
175
122
  (d) => d.depth > maxRecommendedDepth
176
123
  ).length;
@@ -180,7 +127,7 @@ async function analyzeAgentGrounding(options) {
180
127
  let vagueFileNames = 0;
181
128
  for (const f of files) {
182
129
  const base = (0, import_path.basename)(f, (0, import_path.extname)(f)).toLowerCase();
183
- if (VAGUE_FILE_NAMES.has(base) || additionalVague.has(base)) {
130
+ if (import_core.VAGUE_FILE_NAMES.has(base) || additionalVague.has(base)) {
184
131
  vagueFileNames++;
185
132
  }
186
133
  }
@@ -199,7 +146,14 @@ async function analyzeAgentGrounding(options) {
199
146
  let barrelExports = 0;
200
147
  let untypedExports = 0;
201
148
  let totalExports = 0;
149
+ let processed = 0;
202
150
  for (const f of files) {
151
+ processed++;
152
+ options.onProgress?.(
153
+ processed,
154
+ files.length,
155
+ `agent-grounding: analyzing files`
156
+ );
203
157
  const analysis = analyzeFile(f);
204
158
  if (analysis.isBarrel) barrelExports++;
205
159
  untypedExports += analysis.untypedExports;
package/dist/cli.mjs CHANGED
@@ -2,7 +2,7 @@
2
2
  import {
3
3
  analyzeAgentGrounding,
4
4
  calculateGroundingScore
5
- } from "./chunk-NHDH733I.mjs";
5
+ } from "./chunk-NXIMJNCK.mjs";
6
6
 
7
7
  // src/cli.ts
8
8
  import { Command } from "commander";
package/dist/index.js CHANGED
@@ -26,70 +26,10 @@ __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 = [
57
- "node_modules",
58
- "dist",
59
- ".git",
60
- "coverage",
61
- ".turbo",
62
- "build"
63
- ];
64
- function collectEntries(dir, options, depth = 0, dirs = [], files = []) {
65
- if (depth > (options.maxDepth ?? 20)) return { dirs, files };
66
- const excludes = [...DEFAULT_EXCLUDES, ...options.exclude ?? []];
67
- let entries;
68
- try {
69
- entries = (0, import_fs.readdirSync)(dir);
70
- } catch {
71
- return { dirs, files };
72
- }
73
- for (const entry of entries) {
74
- if (excludes.some((ex) => entry === ex || entry.includes(ex))) continue;
75
- const full = (0, import_path.join)(dir, entry);
76
- let stat;
77
- try {
78
- stat = (0, import_fs.statSync)(full);
79
- } catch {
80
- continue;
81
- }
82
- if (stat.isDirectory()) {
83
- dirs.push({ path: full, depth });
84
- collectEntries(full, options, depth + 1, dirs, files);
85
- } else if (stat.isFile() && SUPPORTED_EXTENSIONS.has((0, import_path.extname)(full))) {
86
- if (!options.include || options.include.some((p) => full.includes(p))) {
87
- files.push(full);
88
- }
89
- }
90
- }
91
- return { dirs, files };
92
- }
93
33
  function analyzeFile(filePath) {
94
34
  let code;
95
35
  try {
@@ -169,7 +109,14 @@ async function analyzeAgentGrounding(options) {
169
109
  const rootDir = options.rootDir;
170
110
  const maxRecommendedDepth = options.maxRecommendedDepth ?? 4;
171
111
  const readmeStaleDays = options.readmeStaleDays ?? 90;
172
- const { dirs, files } = collectEntries(rootDir, options);
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
+ }));
173
120
  const deepDirectories = dirs.filter(
174
121
  (d) => d.depth > maxRecommendedDepth
175
122
  ).length;
@@ -179,7 +126,7 @@ async function analyzeAgentGrounding(options) {
179
126
  let vagueFileNames = 0;
180
127
  for (const f of files) {
181
128
  const base = (0, import_path.basename)(f, (0, import_path.extname)(f)).toLowerCase();
182
- if (VAGUE_FILE_NAMES.has(base) || additionalVague.has(base)) {
129
+ if (import_core.VAGUE_FILE_NAMES.has(base) || additionalVague.has(base)) {
183
130
  vagueFileNames++;
184
131
  }
185
132
  }
@@ -198,7 +145,14 @@ async function analyzeAgentGrounding(options) {
198
145
  let barrelExports = 0;
199
146
  let untypedExports = 0;
200
147
  let totalExports = 0;
148
+ let processed = 0;
201
149
  for (const f of files) {
150
+ processed++;
151
+ options.onProgress?.(
152
+ processed,
153
+ files.length,
154
+ `agent-grounding: analyzing files`
155
+ );
202
156
  const analysis = analyzeFile(f);
203
157
  if (analysis.isBarrel) barrelExports++;
204
158
  untypedExports += analysis.untypedExports;
package/dist/index.mjs CHANGED
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  analyzeAgentGrounding,
3
3
  calculateGroundingScore
4
- } from "./chunk-NHDH733I.mjs";
4
+ } from "./chunk-NXIMJNCK.mjs";
5
5
  export {
6
6
  analyzeAgentGrounding,
7
7
  calculateGroundingScore
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aiready/agent-grounding",
3
- "version": "0.1.6",
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",
@@ -40,7 +40,7 @@
40
40
  "chalk": "^5.3.0",
41
41
  "commander": "^14.0.0",
42
42
  "glob": "^13.0.0",
43
- "@aiready/core": "0.9.33"
43
+ "@aiready/core": "0.9.35"
44
44
  },
45
45
  "devDependencies": {
46
46
  "@types/node": "^24.0.0",
package/src/analyzer.ts CHANGED
@@ -9,8 +9,13 @@
9
9
  * 5. Domain consistency — is the same concept named the same everywhere?
10
10
  */
11
11
 
12
- import { readdirSync, statSync, existsSync, readFileSync } from 'fs';
13
- import { join, extname, basename } from 'path';
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
21
  import type {
@@ -18,89 +23,6 @@ import type {
18
23
  AgentGroundingIssue,
19
24
  AgentGroundingReport,
20
25
  } from './types';
21
- import { calculateAgentGrounding } from '@aiready/core';
22
-
23
- // File names that don't describe purpose — an agent can't determine what to find here
24
- const VAGUE_FILE_NAMES = new Set([
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',
45
- ]);
46
-
47
- const SUPPORTED_EXTENSIONS = new Set(['.ts', '.tsx', '.js', '.jsx']);
48
- const DEFAULT_EXCLUDES = [
49
- 'node_modules',
50
- 'dist',
51
- '.git',
52
- 'coverage',
53
- '.turbo',
54
- 'build',
55
- ];
56
-
57
- // ---------------------------------------------------------------------------
58
- // File/dir collection
59
- // ---------------------------------------------------------------------------
60
-
61
- interface DirEntry {
62
- path: string;
63
- depth: number;
64
- }
65
-
66
- function collectEntries(
67
- dir: string,
68
- options: AgentGroundingOptions,
69
- depth = 0,
70
- dirs: DirEntry[] = [],
71
- files: string[] = []
72
- ): { dirs: DirEntry[]; files: string[] } {
73
- if (depth > (options.maxDepth ?? 20)) return { dirs, files };
74
- const excludes = [...DEFAULT_EXCLUDES, ...(options.exclude ?? [])];
75
-
76
- let entries: string[];
77
- try {
78
- entries = readdirSync(dir);
79
- } catch {
80
- return { dirs, files };
81
- }
82
-
83
- for (const entry of entries) {
84
- if (excludes.some((ex) => entry === ex || entry.includes(ex))) continue;
85
- const full = join(dir, entry);
86
- let stat;
87
- try {
88
- stat = statSync(full);
89
- } catch {
90
- continue;
91
- }
92
- if (stat.isDirectory()) {
93
- dirs.push({ path: full, depth });
94
- collectEntries(full, options, depth + 1, dirs, files);
95
- } else if (stat.isFile() && SUPPORTED_EXTENSIONS.has(extname(full))) {
96
- if (!options.include || options.include.some((p) => full.includes(p))) {
97
- files.push(full);
98
- }
99
- }
100
- }
101
-
102
- return { dirs, files };
103
- }
104
26
 
105
27
  // ---------------------------------------------------------------------------
106
28
  // Per-file analysis
@@ -229,11 +151,20 @@ export async function analyzeAgentGrounding(
229
151
  const maxRecommendedDepth = options.maxRecommendedDepth ?? 4;
230
152
  const readmeStaleDays = options.readmeStaleDays ?? 90;
231
153
 
232
- const { dirs, files } = collectEntries(rootDir, options);
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
+ }));
233
164
 
234
165
  // Structure clarity
235
166
  const deepDirectories = dirs.filter(
236
- (d) => d.depth > maxRecommendedDepth
167
+ (d: { path: string; depth: number }) => d.depth > maxRecommendedDepth
237
168
  ).length;
238
169
 
239
170
  // Self-documentation — vague file names
@@ -268,7 +199,15 @@ export async function analyzeAgentGrounding(
268
199
  let untypedExports = 0;
269
200
  let totalExports = 0;
270
201
 
202
+ let processed = 0;
271
203
  for (const f of files) {
204
+ processed++;
205
+ options.onProgress?.(
206
+ processed,
207
+ files.length,
208
+ `agent-grounding: analyzing files`
209
+ );
210
+
272
211
  const analysis = analyzeFile(f);
273
212
  if (analysis.isBarrel) barrelExports++;
274
213
  untypedExports += analysis.untypedExports;