@aiready/agent-grounding 0.1.1

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/index.js ADDED
@@ -0,0 +1,333 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ analyzeAgentGrounding: () => analyzeAgentGrounding,
24
+ calculateGroundingScore: () => calculateGroundingScore
25
+ });
26
+ module.exports = __toCommonJS(index_exports);
27
+
28
+ // src/analyzer.ts
29
+ var import_fs = require("fs");
30
+ var import_path = require("path");
31
+ 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
+ function analyzeFile(filePath) {
87
+ let code;
88
+ try {
89
+ code = (0, import_fs.readFileSync)(filePath, "utf-8");
90
+ } catch {
91
+ return { isBarrel: false, exportedNames: [], untypedExports: 0, totalExports: 0, domainTerms: [] };
92
+ }
93
+ let ast;
94
+ try {
95
+ ast = (0, import_typescript_estree.parse)(code, {
96
+ jsx: filePath.endsWith(".tsx") || filePath.endsWith(".jsx"),
97
+ range: false,
98
+ loc: false
99
+ });
100
+ } catch {
101
+ return { isBarrel: false, exportedNames: [], untypedExports: 0, totalExports: 0, domainTerms: [] };
102
+ }
103
+ let isBarrel = false;
104
+ let exportedNames = [];
105
+ let untypedExports = 0;
106
+ let totalExports = 0;
107
+ const domainTerms = [];
108
+ for (const node of ast.body) {
109
+ if (node.type === "ExportAllDeclaration") {
110
+ isBarrel = true;
111
+ continue;
112
+ }
113
+ if (node.type === "ExportNamedDeclaration") {
114
+ totalExports++;
115
+ const decl = node.declaration;
116
+ if (decl) {
117
+ const name = decl.id?.name ?? decl.declarations?.[0]?.id?.name;
118
+ if (name) {
119
+ exportedNames.push(name);
120
+ domainTerms.push(...name.replace(/([A-Z])/g, " $1").toLowerCase().split(/\s+/).filter(Boolean));
121
+ const hasType = decl.returnType != null || decl.declarations?.[0]?.id?.typeAnnotation != null || decl.typeParameters != null;
122
+ if (!hasType) untypedExports++;
123
+ }
124
+ } else if (node.specifiers && node.specifiers.length > 0) {
125
+ isBarrel = true;
126
+ }
127
+ }
128
+ if (node.type === "ExportDefaultDeclaration") {
129
+ totalExports++;
130
+ }
131
+ }
132
+ return { isBarrel, exportedNames, untypedExports, totalExports, domainTerms };
133
+ }
134
+ function detectInconsistentTerms(allTerms) {
135
+ const termFreq = /* @__PURE__ */ new Map();
136
+ for (const term of allTerms) {
137
+ if (term.length >= 3) {
138
+ termFreq.set(term, (termFreq.get(term) ?? 0) + 1);
139
+ }
140
+ }
141
+ const orphans = [...termFreq.values()].filter((count) => count === 1).length;
142
+ const common = [...termFreq.values()].filter((count) => count >= 3).length;
143
+ const vocabularySize = termFreq.size;
144
+ const inconsistent = Math.max(0, orphans - common * 2);
145
+ return { inconsistent, vocabularySize };
146
+ }
147
+ async function analyzeAgentGrounding(options) {
148
+ const rootDir = options.rootDir;
149
+ const maxRecommendedDepth = options.maxRecommendedDepth ?? 4;
150
+ 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()));
154
+ let vagueFileNames = 0;
155
+ for (const f of files) {
156
+ const base = (0, import_path.basename)(f, (0, import_path.extname)(f)).toLowerCase();
157
+ if (VAGUE_FILE_NAMES.has(base) || additionalVague.has(base)) {
158
+ vagueFileNames++;
159
+ }
160
+ }
161
+ const readmePath = (0, import_path.join)(rootDir, "README.md");
162
+ const hasRootReadme = (0, import_fs.existsSync)(readmePath);
163
+ let readmeIsFresh = false;
164
+ if (hasRootReadme) {
165
+ try {
166
+ const stat = (0, import_fs.statSync)(readmePath);
167
+ const ageDays = (Date.now() - stat.mtimeMs) / (1e3 * 60 * 60 * 24);
168
+ readmeIsFresh = ageDays < readmeStaleDays;
169
+ } catch {
170
+ }
171
+ }
172
+ const allDomainTerms = [];
173
+ let barrelExports = 0;
174
+ let untypedExports = 0;
175
+ let totalExports = 0;
176
+ for (const f of files) {
177
+ const analysis = analyzeFile(f);
178
+ if (analysis.isBarrel) barrelExports++;
179
+ untypedExports += analysis.untypedExports;
180
+ totalExports += analysis.totalExports;
181
+ allDomainTerms.push(...analysis.domainTerms);
182
+ }
183
+ const { inconsistent: inconsistentDomainTerms, vocabularySize: domainVocabularySize } = detectInconsistentTerms(allDomainTerms);
184
+ const groundingResult = (0, import_core.calculateAgentGrounding)({
185
+ deepDirectories,
186
+ totalDirectories: dirs.length,
187
+ vagueFileNames,
188
+ totalFiles: files.length,
189
+ hasRootReadme,
190
+ readmeIsFresh,
191
+ barrelExports,
192
+ untypedExports,
193
+ totalExports: Math.max(1, totalExports),
194
+ inconsistentDomainTerms,
195
+ domainVocabularySize: Math.max(1, domainVocabularySize)
196
+ });
197
+ const issues = [];
198
+ if (groundingResult.dimensions.structureClarityScore < 70) {
199
+ issues.push({
200
+ type: "agent-navigation-failure",
201
+ dimension: "structure-clarity",
202
+ severity: "major",
203
+ message: `${deepDirectories} directories exceed recommended depth of ${maxRecommendedDepth} \u2014 agents struggle to navigate deep trees.`,
204
+ location: { file: rootDir, line: 0 },
205
+ suggestion: `Flatten nested directories to ${maxRecommendedDepth} levels or fewer.`
206
+ });
207
+ }
208
+ if (groundingResult.dimensions.selfDocumentationScore < 70) {
209
+ issues.push({
210
+ type: "agent-navigation-failure",
211
+ dimension: "self-documentation",
212
+ severity: "major",
213
+ message: `${vagueFileNames} files use vague names (utils, helpers, misc) \u2014 an agent cannot determine their purpose from the name alone.`,
214
+ location: { file: rootDir, line: 0 },
215
+ suggestion: "Rename to domain-specific names: e.g., userAuthUtils \u2192 tokenValidator."
216
+ });
217
+ }
218
+ if (!hasRootReadme) {
219
+ issues.push({
220
+ type: "agent-navigation-failure",
221
+ dimension: "entry-point",
222
+ severity: "critical",
223
+ message: "No root README.md found \u2014 agents have no orientation document to start from.",
224
+ location: { file: (0, import_path.join)(rootDir, "README.md"), line: 0 },
225
+ suggestion: "Add a README.md explaining the project structure, entry points, and key conventions."
226
+ });
227
+ } else if (!readmeIsFresh) {
228
+ issues.push({
229
+ type: "agent-navigation-failure",
230
+ dimension: "entry-point",
231
+ severity: "minor",
232
+ message: `README.md is stale (>${readmeStaleDays} days without updates) \u2014 agents may be misled by outdated context.`,
233
+ location: { file: readmePath, line: 0 },
234
+ suggestion: "Update README.md to reflect the current codebase structure."
235
+ });
236
+ }
237
+ if (groundingResult.dimensions.apiClarityScore < 70) {
238
+ issues.push({
239
+ type: "agent-navigation-failure",
240
+ dimension: "api-clarity",
241
+ severity: "major",
242
+ message: `${untypedExports} of ${totalExports} public exports lack TypeScript type annotations \u2014 agents cannot infer the API contract.`,
243
+ location: { file: rootDir, line: 0 },
244
+ suggestion: "Add explicit return type and parameter annotations to all exported functions."
245
+ });
246
+ }
247
+ if (groundingResult.dimensions.domainConsistencyScore < 70) {
248
+ issues.push({
249
+ type: "agent-navigation-failure",
250
+ dimension: "domain-consistency",
251
+ severity: "major",
252
+ message: `${inconsistentDomainTerms} domain terms appear to be used inconsistently \u2014 agents get confused when one concept has multiple names.`,
253
+ location: { file: rootDir, line: 0 },
254
+ suggestion: "Establish a domain glossary and enforce one term per concept across the codebase."
255
+ });
256
+ }
257
+ return {
258
+ summary: {
259
+ filesAnalyzed: files.length,
260
+ directoriesAnalyzed: dirs.length,
261
+ score: groundingResult.score,
262
+ rating: groundingResult.rating,
263
+ dimensions: groundingResult.dimensions
264
+ },
265
+ issues,
266
+ rawData: {
267
+ deepDirectories,
268
+ totalDirectories: dirs.length,
269
+ vagueFileNames,
270
+ totalFiles: files.length,
271
+ hasRootReadme,
272
+ readmeIsFresh,
273
+ barrelExports,
274
+ untypedExports,
275
+ totalExports,
276
+ inconsistentDomainTerms,
277
+ domainVocabularySize
278
+ },
279
+ recommendations: groundingResult.recommendations
280
+ };
281
+ }
282
+
283
+ // src/scoring.ts
284
+ function calculateGroundingScore(report) {
285
+ const { summary, rawData, issues, recommendations } = report;
286
+ const factors = [
287
+ {
288
+ name: "Structure Clarity",
289
+ impact: Math.round(summary.dimensions.structureClarityScore - 50),
290
+ description: `${rawData.deepDirectories} of ${rawData.totalDirectories} dirs exceed recommended depth`
291
+ },
292
+ {
293
+ name: "Self-Documentation",
294
+ impact: Math.round(summary.dimensions.selfDocumentationScore - 50),
295
+ description: `${rawData.vagueFileNames} of ${rawData.totalFiles} files have vague names`
296
+ },
297
+ {
298
+ name: "Entry Points",
299
+ impact: Math.round(summary.dimensions.entryPointScore - 50),
300
+ description: rawData.hasRootReadme ? rawData.readmeIsFresh ? "README present and fresh" : "README present but stale" : "No root README"
301
+ },
302
+ {
303
+ name: "API Clarity",
304
+ impact: Math.round(summary.dimensions.apiClarityScore - 50),
305
+ description: `${rawData.untypedExports} of ${rawData.totalExports} exports lack type annotations`
306
+ },
307
+ {
308
+ name: "Domain Consistency",
309
+ impact: Math.round(summary.dimensions.domainConsistencyScore - 50),
310
+ description: `${rawData.inconsistentDomainTerms} inconsistent domain terms detected`
311
+ }
312
+ ];
313
+ const recs = recommendations.map((action) => ({
314
+ action,
315
+ estimatedImpact: 6,
316
+ priority: summary.score < 50 ? "high" : "medium"
317
+ }));
318
+ return {
319
+ toolName: "agent-grounding",
320
+ score: summary.score,
321
+ rawMetrics: {
322
+ ...rawData,
323
+ rating: summary.rating
324
+ },
325
+ factors,
326
+ recommendations: recs
327
+ };
328
+ }
329
+ // Annotate the CommonJS export names for ESM import in node:
330
+ 0 && (module.exports = {
331
+ analyzeAgentGrounding,
332
+ calculateGroundingScore
333
+ });
package/dist/index.mjs ADDED
@@ -0,0 +1,8 @@
1
+ import {
2
+ analyzeAgentGrounding,
3
+ calculateGroundingScore
4
+ } from "./chunk-OOB3JMXQ.mjs";
5
+ export {
6
+ analyzeAgentGrounding,
7
+ calculateGroundingScore
8
+ };
package/package.json ADDED
@@ -0,0 +1,62 @@
1
+ {
2
+ "name": "@aiready/agent-grounding",
3
+ "version": "0.1.1",
4
+ "description": "Measures how well an AI agent can navigate a codebase autonomously without human assistance",
5
+ "main": "./dist/index.js",
6
+ "module": "./dist/index.mjs",
7
+ "types": "./dist/index.d.ts",
8
+ "bin": {
9
+ "aiready-agent-grounding": "./dist/cli.js"
10
+ },
11
+ "exports": {
12
+ ".": {
13
+ "types": "./dist/index.d.ts",
14
+ "require": "./dist/index.js",
15
+ "import": "./dist/index.mjs"
16
+ }
17
+ },
18
+ "keywords": [
19
+ "aiready",
20
+ "agent-grounding",
21
+ "ai-navigation",
22
+ "code-structure",
23
+ "copilot",
24
+ "claude",
25
+ "chatgpt",
26
+ "ai-assisted-development",
27
+ "autonomous-agents",
28
+ "codebase-navigation"
29
+ ],
30
+ "author": "AIReady Team",
31
+ "license": "MIT",
32
+ "repository": {
33
+ "type": "git",
34
+ "url": "https://github.com/caopengau/aiready-agent-grounding.git"
35
+ },
36
+ "homepage": "https://github.com/caopengau/aiready-agent-grounding",
37
+ "dependencies": {
38
+ "@typescript-eslint/types": "^8.53.0",
39
+ "@typescript-eslint/typescript-estree": "^8.53.0",
40
+ "chalk": "^5.3.0",
41
+ "commander": "^14.0.0",
42
+ "glob": "^11.0.0",
43
+ "@aiready/core": "0.9.28"
44
+ },
45
+ "devDependencies": {
46
+ "@types/node": "^24.0.0",
47
+ "tsup": "^8.3.5",
48
+ "typescript": "^5.7.2",
49
+ "vitest": "^4.0.0"
50
+ },
51
+ "engines": {
52
+ "node": ">=18.0.0"
53
+ },
54
+ "scripts": {
55
+ "build": "tsup src/index.ts src/cli.ts --format cjs,esm --dts",
56
+ "dev": "tsup src/index.ts src/cli.ts --format cjs,esm --dts --watch",
57
+ "test": "vitest run",
58
+ "lint": "eslint src",
59
+ "clean": "rm -rf dist",
60
+ "release": "pnpm build && pnpm publish --no-git-checks"
61
+ }
62
+ }
@@ -0,0 +1,77 @@
1
+ import { analyzeAgentGrounding } from '../analyzer';
2
+ import { join } from 'path';
3
+ import { writeFileSync, mkdirSync, rmSync } from 'fs';
4
+ import { tmpdir } from 'os';
5
+ import { describe, it, expect, beforeAll, afterAll } from 'vitest';
6
+
7
+ describe('Agent Grounding Analyzer', () => {
8
+ const tmpDir = join(tmpdir(), 'aiready-ag-tests');
9
+
10
+ beforeAll(() => {
11
+ mkdirSync(tmpDir, { recursive: true });
12
+ });
13
+
14
+ afterAll(() => {
15
+ rmSync(tmpDir, { recursive: true, force: true });
16
+ });
17
+
18
+ function createTestFile(name: string, content: string): string {
19
+ const filePath = join(tmpDir, name);
20
+ // Ensure dir exists
21
+ const dir = join(filePath, '..');
22
+ mkdirSync(dir, { recursive: true });
23
+
24
+ writeFileSync(filePath, content, 'utf8');
25
+ return filePath;
26
+ }
27
+
28
+ describe('Deep Directories and Vague Files', () => {
29
+ it('should detect deep directories and vague file names', async () => {
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;');
32
+ createTestFile('src/utils.ts', 'export const y = 2;');
33
+
34
+ const report = await analyzeAgentGrounding({
35
+ rootDir: tmpDir,
36
+ maxRecommendedDepth: 3,
37
+ additionalVagueNames: ['utils', 'helpers']
38
+ });
39
+
40
+ expect(report.issues.length).toBeGreaterThanOrEqual(1);
41
+
42
+ const deepIssues = report.issues.filter(i => i.dimension === 'structure-clarity' && i.message.includes('exceed'));
43
+ // The deep.ts file contributes to the aggregate depth count
44
+ expect(deepIssues.length).toBeGreaterThan(0);
45
+
46
+ const vagueIssues = report.issues.filter(i => i.dimension === 'self-documentation');
47
+ expect(vagueIssues.some(i => i.message.includes('vague names'))).toBe(true);
48
+ });
49
+ });
50
+
51
+ describe('Missing READMEs', () => {
52
+ it('should detect missing READMEs in directories', async () => {
53
+ // tmpDir has no README
54
+ const report = await analyzeAgentGrounding({ rootDir: tmpDir });
55
+
56
+ const issues = report.issues;
57
+ const readmeIssues = issues.filter(i => i.dimension === 'entry-point' || i.message.includes('README'));
58
+
59
+ expect(readmeIssues.length).toBeGreaterThan(0);
60
+ });
61
+ });
62
+
63
+ describe('Untyped Exports', () => {
64
+ 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; }');
67
+
68
+ const report = await analyzeAgentGrounding({ rootDir: tmpDir });
69
+
70
+ const issues = report.issues;
71
+ const untypedIssues = issues.filter(i => i.dimension === 'api-clarity');
72
+
73
+ // The JS file untyped export contributes to the aggregate count
74
+ expect(untypedIssues.some(i => i.message.includes('lack TypeScript type'))).toBe(true);
75
+ });
76
+ });
77
+ });