Skip to content

Commit 130237b

Browse files
jrenaldi79claude
andauthored
Strengthen auto-doc coverage in both eval suites (#27)
Level-5 fixture now has proper auto-doc infrastructure: - scripts/generate-docs.js and generate-docs-helpers.js (functional) - AUTO:tree and AUTO:modules markers (generated from real filesystem) - docs/index.md - generate-docs.js --check passes against fixture Readiness eval config: - Level-3 must recommend "auto-gen" (missing auto-doc) - Level-5 must NOT recommend "add auto-generated sections" (has it) https://claude.ai/code/session_01Hbxy31TkbujzukGFSxLcPw Co-authored-by: Claude <[email protected]>
1 parent e6d4de8 commit 130237b

5 files changed

Lines changed: 601 additions & 14 deletions

File tree

tests/evals/eval-config.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@
5656
"agentic-workflow": { "pass_max": 0 }
5757
},
5858
"recommendations": {
59-
"must_mention": ["secret scanning", ".env.example", "TDD", "agentic workflow"],
59+
"must_mention": ["secret scanning", ".env.example", "TDD", "agentic workflow", "auto-gen"],
6060
"must_not_mention": ["run /setup"]
6161
}
6262
}
@@ -82,7 +82,7 @@
8282
"agentic-workflow": { "pass_min": 1 }
8383
},
8484
"recommendations": {
85-
"must_not_mention": ["run /setup", "install linter", "install test runner"]
85+
"must_not_mention": ["run /setup", "install linter", "install test runner", "add auto-generated sections"]
8686
}
8787
}
8888
}

tests/evals/fixtures/level-5-autonomous/CLAUDE.md

Lines changed: 22 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -14,19 +14,29 @@
1414

1515
## Architecture
1616

17-
<!-- AUTO:architecture -->
18-
```
17+
<!-- AUTO:tree -->
18+
scripts/
19+
├── generate-docs-helpers.js # Helper functions for generate-docs.js.
20+
└── generate-docs.js # Auto-generate CLAUDE.md sections from source code.
1921
src/
20-
index.ts — Entry point, starts Express server
21-
app.ts — Express app factory (createApp)
22-
app.test.ts — App integration tests
23-
routes/
24-
health.ts — GET /health endpoint
25-
health.test.ts — Health endpoint tests
26-
users.ts — CRUD /users endpoints
27-
users.test.ts — Users endpoint tests
28-
```
29-
<!-- /AUTO:architecture -->
22+
├── routes/
23+
│ ├── health.test.ts
24+
│ ├── health.ts
25+
│ ├── users.test.ts
26+
│ └── users.ts
27+
├── app.test.ts
28+
├── app.ts
29+
└── index.ts
30+
<!-- /AUTO:tree -->
31+
32+
## Key Modules
33+
34+
<!-- AUTO:modules -->
35+
| Module | Purpose | Key Exports |
36+
|--------|---------|-------------|
37+
| `scripts/generate-docs-helpers.js` | Helper functions for generate-docs.js. | `name()` |
38+
| `scripts/generate-docs.js` | Auto-generate CLAUDE.md sections from source code. | `replaceMarkers()`, `validateCrossLinks()`, `buildDocsIndex()`, `checkMarkersAreCurrent()` |
39+
<!-- /AUTO:modules -->
3040

3141
## Critical Gotchas
3242

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# Documentation Index
2+
3+
No documentation files found.
Lines changed: 290 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,290 @@
1+
/**
2+
* Helper functions for generate-docs.js.
3+
*
4+
* Extracts JSDoc descriptions, export names, builds directory trees,
5+
* and collects module index data from source files.
6+
*/
7+
8+
const fs = require('node:fs');
9+
const path = require('node:path');
10+
11+
const SKIP_DIRS = new Set(['node_modules', '.git', 'coverage', 'dist', 'build', 'fixtures', 'results']);
12+
13+
// ---------------------------------------------------------------------------
14+
// JSDoc & Export Extraction
15+
// ---------------------------------------------------------------------------
16+
17+
/**
18+
* Extract the first description line from a file's JSDoc comment.
19+
* @param {string} filePath - Absolute path to a .js file
20+
* @returns {string} Description text, or empty string
21+
*/
22+
function extractJSDocDescription(filePath) {
23+
let content;
24+
try {
25+
content = fs.readFileSync(filePath, 'utf-8');
26+
} catch {
27+
return '';
28+
}
29+
30+
// Multi-line first (file-level JSDoc is usually multi-line): /** \n * desc \n */
31+
const multiLine = content.match(/\/\*\*\s*\n([\s\S]*?)\*\//);
32+
if (multiLine) {
33+
const lines = multiLine[1].split('\n');
34+
for (const line of lines) {
35+
const cleaned = line.replace(/^\s*\*\s?/, '').trim();
36+
if (cleaned && !cleaned.startsWith('@')) {
37+
return cleaned;
38+
}
39+
}
40+
}
41+
42+
// Single-line fallback: /** desc */
43+
const singleLine = content.match(/\/\*\*\s+(.+?)\s*\*\//);
44+
if (singleLine) {
45+
const text = singleLine[1].replace(/\s*\*\/$/, '').trim();
46+
if (!text.startsWith('@')) return text;
47+
}
48+
return '';
49+
}
50+
51+
/**
52+
* Extract exported names from a CommonJS module (capped at 5).
53+
* Reads `module.exports = { ... }` and `exports.name =` patterns.
54+
* @param {string} filePath - Absolute path to a .js file
55+
* @returns {string[]} Array of export names (max 5)
56+
*/
57+
function extractExports(filePath) {
58+
let content;
59+
try {
60+
content = fs.readFileSync(filePath, 'utf-8');
61+
} catch {
62+
return [];
63+
}
64+
65+
const names = new Set();
66+
67+
// module.exports = { name1, name2, ... }
68+
const objMatch = content.match(/module\.exports\s*=\s*\{([^}]*)\}/s);
69+
if (objMatch) {
70+
const body = objMatch[1];
71+
const keyRe = /\b([a-zA-Z_$][\w$]*)\b(?:\s*[,:}\n]|\s*$)/g;
72+
let m;
73+
while ((m = keyRe.exec(body)) !== null) {
74+
names.add(m[1]);
75+
}
76+
}
77+
78+
// exports.name = ...
79+
const namedRe = /exports\.([a-zA-Z_$][\w$]*)\s*=/g;
80+
let m;
81+
while ((m = namedRe.exec(content)) !== null) {
82+
names.add(m[1]);
83+
}
84+
85+
return [...names].slice(0, 5);
86+
}
87+
88+
// ---------------------------------------------------------------------------
89+
// Directory Tree Builder
90+
// ---------------------------------------------------------------------------
91+
92+
/**
93+
* Build an ASCII directory tree with JSDoc annotations.
94+
* @param {string} rootDir - Project root directory
95+
* @param {string[]} dirs - Top-level directories to include (e.g. ['src/', 'bin/'])
96+
* @returns {string} ASCII tree string
97+
*/
98+
function buildDirectoryTree(rootDir, dirs) {
99+
const lines = [];
100+
for (const dir of dirs) {
101+
const fullPath = path.join(rootDir, dir);
102+
if (!fs.existsSync(fullPath)) {
103+
continue;
104+
}
105+
lines.push(dir);
106+
buildTreeRecursive(fullPath, '', lines);
107+
}
108+
return lines.join('\n');
109+
}
110+
111+
/**
112+
* Recursively build tree lines for a directory.
113+
* @param {string} dirPath - Directory to scan
114+
* @param {string} prefix - Line prefix for indentation
115+
* @param {string[]} lines - Accumulator for output lines
116+
*/
117+
function buildTreeRecursive(dirPath, prefix, lines) {
118+
let entries;
119+
try {
120+
entries = fs.readdirSync(dirPath, { withFileTypes: true });
121+
} catch {
122+
return;
123+
}
124+
125+
entries = entries.filter(e => {
126+
if (e.name.startsWith('.')) {
127+
return false;
128+
}
129+
if (e.isDirectory() && SKIP_DIRS.has(e.name)) {
130+
return false;
131+
}
132+
return true;
133+
});
134+
135+
const sortedDirs = entries.filter(e => e.isDirectory()).sort((a, b) => a.name.localeCompare(b.name));
136+
const sortedFiles = entries.filter(e => e.isFile()).sort((a, b) => a.name.localeCompare(b.name));
137+
const sorted = [...sortedDirs, ...sortedFiles];
138+
139+
for (let i = 0; i < sorted.length; i++) {
140+
const entry = sorted[i];
141+
const isLast = i === sorted.length - 1;
142+
const connector = isLast ? '\u2514\u2500\u2500 ' : '\u251c\u2500\u2500 ';
143+
const childPrefix = isLast ? ' ' : '\u2502 ';
144+
145+
if (entry.isDirectory()) {
146+
lines.push(`${prefix}${connector}${entry.name}/`);
147+
buildTreeRecursive(path.join(dirPath, entry.name), prefix + childPrefix, lines);
148+
} else {
149+
let annotation = '';
150+
if (entry.name.endsWith('.js')) {
151+
const desc = extractJSDocDescription(path.join(dirPath, entry.name));
152+
if (desc) {
153+
annotation = ` # ${desc}`;
154+
}
155+
}
156+
lines.push(`${prefix}${connector}${entry.name}${annotation}`);
157+
}
158+
}
159+
}
160+
161+
// ---------------------------------------------------------------------------
162+
// Source Directory Detection
163+
// ---------------------------------------------------------------------------
164+
165+
const KNOWN_SOURCE_DIRS = new Set([
166+
'src', 'lib', 'app', 'apps', 'packages', 'services', 'modules',
167+
'cmd', 'internal', 'pkg', 'bin', 'components',
168+
'scripts', 'tests', 'test', 'spec',
169+
]);
170+
171+
const SOURCE_EXTENSIONS = new Set([
172+
'.js', '.ts', '.tsx', '.jsx', '.mjs', '.cjs',
173+
'.py', '.go', '.rs', '.c', '.cpp', '.h', '.hpp',
174+
'.java', '.kt', '.rb', '.php', '.swift', '.cs',
175+
]);
176+
177+
/**
178+
* Detect source directories in a project by name and content.
179+
* @param {string} rootDir - Project root directory
180+
* @returns {string[]} Array of directory names with trailing slash
181+
*/
182+
function detectSourceDirs(rootDir) {
183+
let entries;
184+
try {
185+
entries = fs.readdirSync(rootDir, { withFileTypes: true });
186+
} catch {
187+
return [];
188+
}
189+
190+
const dirs = [];
191+
for (const entry of entries) {
192+
if (!entry.isDirectory() || entry.name.startsWith('.') || SKIP_DIRS.has(entry.name)) {
193+
continue;
194+
}
195+
if (KNOWN_SOURCE_DIRS.has(entry.name)) {
196+
if (dirHasFiles(path.join(rootDir, entry.name), false)) {
197+
dirs.push(`${entry.name}/`);
198+
}
199+
continue;
200+
}
201+
if (dirHasFiles(path.join(rootDir, entry.name), true)) {
202+
dirs.push(`${entry.name}/`);
203+
}
204+
}
205+
return dirs.sort();
206+
}
207+
208+
function dirHasFiles(dirPath, sourceOnly) {
209+
try {
210+
for (const e of fs.readdirSync(dirPath, { withFileTypes: true })) {
211+
if (e.isFile()) {
212+
if (!sourceOnly) return true;
213+
if (SOURCE_EXTENSIONS.has(path.extname(e.name).toLowerCase())) return true;
214+
}
215+
if (e.isDirectory() && !SKIP_DIRS.has(e.name) && dirHasFiles(path.join(dirPath, e.name), sourceOnly)) {
216+
return true;
217+
}
218+
}
219+
} catch { /* empty */ }
220+
return false;
221+
}
222+
223+
// ---------------------------------------------------------------------------
224+
// Module Index Builder
225+
// ---------------------------------------------------------------------------
226+
227+
/**
228+
* Build a markdown table of modules from all detected source directories.
229+
* @param {string} rootDir - Project root directory
230+
* @returns {string} Markdown table
231+
*/
232+
function buildModuleIndex(rootDir) {
233+
const sourceDirs = detectSourceDirs(rootDir);
234+
const rows = [];
235+
for (const dir of sourceDirs) {
236+
const dirName = dir.replace(/\/$/, '');
237+
const fullPath = path.join(rootDir, dirName);
238+
collectModules(fullPath, rows, dirName);
239+
}
240+
241+
const header = '| Module | Purpose | Key Exports |';
242+
const sep = '|--------|---------|-------------|';
243+
const dataRows = rows.map(r => `| \`${r.module}\` | ${r.purpose} | ${r.exports} |`);
244+
return [header, sep, ...dataRows].join('\n');
245+
}
246+
247+
/**
248+
* Recursively collect module info from a directory.
249+
* @param {string} dirPath - Directory to scan
250+
* @param {Array<{module: string, purpose: string, exports: string}>} rows
251+
* @param {string} [relPrefix=''] - Relative path prefix
252+
*/
253+
function collectModules(dirPath, rows, relPrefix = '') {
254+
let entries;
255+
try {
256+
entries = fs.readdirSync(dirPath, { withFileTypes: true });
257+
} catch {
258+
return;
259+
}
260+
261+
const sortedDirs = entries.filter(e => e.isDirectory() && !SKIP_DIRS.has(e.name) && !e.name.startsWith('.'))
262+
.sort((a, b) => a.name.localeCompare(b.name));
263+
const sortedFiles = entries.filter(e => e.isFile() && e.name.endsWith('.js'))
264+
.sort((a, b) => a.name.localeCompare(b.name));
265+
266+
for (const file of sortedFiles) {
267+
const fullPath = path.join(dirPath, file.name);
268+
const modulePath = relPrefix ? `${relPrefix}/${file.name}` : file.name;
269+
const desc = extractJSDocDescription(fullPath);
270+
const exps = extractExports(fullPath);
271+
rows.push({
272+
module: modulePath,
273+
purpose: desc || '',
274+
exports: exps.map(e => `\`${e}()\``).join(', '),
275+
});
276+
}
277+
278+
for (const dir of sortedDirs) {
279+
collectModules(path.join(dirPath, dir.name), rows, relPrefix ? `${relPrefix}/${dir.name}` : dir.name);
280+
}
281+
}
282+
283+
module.exports = {
284+
SKIP_DIRS,
285+
extractJSDocDescription,
286+
extractExports,
287+
buildDirectoryTree,
288+
detectSourceDirs,
289+
buildModuleIndex,
290+
};

0 commit comments

Comments
 (0)