@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/dist/cli.mjs CHANGED
@@ -2,16 +2,24 @@
2
2
  import {
3
3
  analyzeAgentGrounding,
4
4
  calculateGroundingScore
5
- } from "./chunk-OOB3JMXQ.mjs";
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 { loadConfig, mergeConfigWithDefaults, resolveOutputPath } from "@aiready/core";
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("Measure how well an AI agent can navigate your codebase autonomously").version("0.1.0").addHelpText("after", `
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
- `).argument("<directory>", "Directory to analyze").option("--max-depth <n>", "Max recommended directory depth (default: 4)", "4").option("--readme-stale-days <n>", "Days after which README is considered stale (default: 90)", "90").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) => {
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, rawData, issues, recommendations } = report;
85
+ const { summary, issues, recommendations } = report;
69
86
  console.log(chalk.bold("\n\u{1F9ED} Agent Grounding Analysis\n"));
70
- console.log(`Score: ${scoreColor(summary.score)(summary.score + "/100")} (${summary.rating.toUpperCase()})`);
71
- console.log(`Files: ${chalk.cyan(summary.filesAnalyzed)} Directories: ${chalk.cyan(summary.directoriesAnalyzed)}`);
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(` ${String(name).padEnd(22)} ${scoreColor(val)(bar)} ${val}/100`);
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) console.log(` ${chalk.dim("\u2192")} ${chalk.italic(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(chalk.green("\n\u2728 No grounding issues found \u2014 agents can navigate freely!\n"));
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((rec, i) => console.log(`${i + 1}. ${rec}`));
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 { isBarrel: false, exportedNames: [], untypedExports: 0, totalExports: 0, domainTerms: [] };
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 { isBarrel: false, exportedNames: [], untypedExports: 0, totalExports: 0, domainTerms: [] };
54
+ return {
55
+ isBarrel: false,
56
+ exportedNames: [],
57
+ untypedExports: 0,
58
+ totalExports: 0,
59
+ domainTerms: []
60
+ };
102
61
  }
103
62
  let isBarrel = false;
104
- let exportedNames = [];
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(...name.replace(/([A-Z])/g, " $1").toLowerCase().split(/\s+/).filter(Boolean));
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 { dirs, files } = collectEntries(rootDir, options);
152
- const deepDirectories = dirs.filter((d) => d.depth > maxRecommendedDepth).length;
153
- const additionalVague = new Set((options.additionalVagueNames ?? []).map((n) => n.toLowerCase()));
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 { inconsistent: inconsistentDomainTerms, vocabularySize: domainVocabularySize } = detectInconsistentTerms(allDomainTerms);
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, issues, recommendations } = report;
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((action) => ({
314
- action,
315
- estimatedImpact: 6,
316
- priority: summary.score < 50 ? "high" : "medium"
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
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  analyzeAgentGrounding,
3
3
  calculateGroundingScore
4
- } from "./chunk-OOB3JMXQ.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.5",
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": "^11.0.0",
43
- "@aiready/core": "0.9.32"
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('src/components/common/utils/helpers/deep/very/deep.ts', 'export const x = 1;');
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(i => i.dimension === 'structure-clarity' && i.message.includes('exceed'));
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(i => i.dimension === 'self-documentation');
47
- expect(vagueIssues.some(i => i.message.includes('vague names'))).toBe(true);
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(i => i.dimension === 'entry-point' || i.message.includes('README'));
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('src/untyped.js', 'export function doSomething(a, b) { return a + b; }');
66
- createTestFile('src/typed.ts', 'export function doSomething(a: number, b: number): number { return a + b; }');
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(untypedIssues.some(i => i.message.includes('lack TypeScript type'))).toBe(true);
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 { 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
- import type { AgentGroundingOptions, AgentGroundingIssue, AgentGroundingReport } from './types';
17
- import { calculateAgentGrounding } from '@aiready/core';
18
-
19
- // File names that don't describe purpose — an agent can't determine what to find here
20
- const VAGUE_FILE_NAMES = new Set([
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 { isBarrel: false, exportedNames: [], untypedExports: 0, totalExports: 0, domainTerms: [] };
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 { isBarrel: false, exportedNames: [], untypedExports: 0, totalExports: 0, domainTerms: [] };
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
- let exportedNames: string[] = [];
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(...name.replace(/([A-Z])/g, ' $1').toLowerCase().split(/\s+/).filter(Boolean));
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[]): { inconsistent: number; vocabularySize: number } {
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
- 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
+ }));
184
164
 
185
165
  // Structure clarity
186
- const deepDirectories = dirs.filter(d => d.depth > maxRecommendedDepth).length;
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((options.additionalVagueNames ?? []).map(n => n.toLowerCase()));
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 { inconsistent: inconsistentDomainTerms, vocabularySize: domainVocabularySize } =
226
- detectInconsistentTerms(allDomainTerms);
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: 'Rename to domain-specific names: e.g., userAuthUtils → tokenValidator.',
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: 'No root README.md found — agents have no orientation document to start from.',
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: 'Add a README.md explaining the project structure, entry points, and key conventions.',
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: 'Add explicit return type and parameter annotations to all exported functions.',
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: 'Establish a domain glossary and enforce one term per concept across the codebase.',
306
+ suggestion:
307
+ 'Establish a domain glossary and enforce one term per concept across the codebase.',
307
308
  });
308
309
  }
309
310