fix: align INTEL_FILES constant with actual agent output filenames (#2225)

The gsd-intel-updater agent writes file-roles.json, api-map.json,
dependency-graph.json, arch-decisions.json, and stack.json. But
INTEL_FILES in intel.cjs declared files.json, apis.json, deps.json,
arch.md, and stack.json. Only stack.json matched. Every query/status/
diff/validate call iterated INTEL_FILES and found nothing, reporting
all intel files as missing even after a successful refresh.

Update INTEL_FILES to use the agent's actual filenames. Remove the
arch.md special-case code paths (mtime-based staleness, text search,
.md skip in validate) since arch-decisions.json is JSON like the rest.
Update all intel tests to use the new canonical filenames.

Closes #2205

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Tom Boucher
2026-04-15 14:59:24 -04:00
committed by GitHub
parent 62261a3166
commit 4f5ffccec7
2 changed files with 58 additions and 75 deletions

View File

@@ -19,10 +19,10 @@ const crypto = require('crypto');
const INTEL_DIR = '.planning/intel';
const INTEL_FILES = {
files: 'files.json',
apis: 'apis.json',
deps: 'deps.json',
arch: 'arch.md',
files: 'file-roles.json',
apis: 'api-map.json',
deps: 'dependency-graph.json',
arch: 'arch-decisions.json',
stack: 'stack.json'
};
@@ -204,10 +204,8 @@ function intelQuery(term, planningDir) {
const matches = [];
let total = 0;
// Search JSON intel files
// Search all JSON intel files
for (const [_key, filename] of Object.entries(INTEL_FILES)) {
if (filename.endsWith('.md')) continue; // Skip arch.md here
const filePath = intelFilePath(planningDir, filename);
const data = safeReadJson(filePath);
if (!data) continue;
@@ -219,14 +217,6 @@ function intelQuery(term, planningDir) {
}
}
// Search arch.md
const archPath = intelFilePath(planningDir, INTEL_FILES.arch);
const archMatches = searchArchMd(archPath, term);
if (archMatches.length > 0) {
matches.push({ source: INTEL_FILES.arch, entries: archMatches });
total += archMatches.length;
}
return { matches, term, total };
}
@@ -257,20 +247,10 @@ function intelStatus(planningDir) {
let updatedAt = null;
if (filename.endsWith('.md')) {
// For arch.md, use file mtime
try {
const stat = fs.statSync(filePath);
updatedAt = stat.mtime.toISOString();
} catch (_e) {
// intentionally silent: fall through on error
}
} else {
// For JSON files, read _meta.updated_at
const data = safeReadJson(filePath);
if (data && data._meta && data._meta.updated_at) {
updatedAt = data._meta.updated_at;
}
// All intel files are JSON — read _meta.updated_at
const data = safeReadJson(filePath);
if (data && data._meta && data._meta.updated_at) {
updatedAt = data._meta.updated_at;
}
let stale = true;
@@ -409,8 +389,7 @@ function intelValidate(planningDir) {
continue;
}
// Skip non-JSON files (arch.md)
if (filename.endsWith('.md')) continue;
// All intel files are JSON — validate _meta and entries structure
// Parse JSON
let data;

View File

@@ -160,7 +160,7 @@ describe('intelQuery', () => {
});
test('finds matches in JSON file keys', () => {
writeIntelJson(planningDir, 'files.json', {
writeIntelJson(planningDir, 'file-roles.json', {
_meta: { updated_at: new Date().toISOString() },
entries: {
'src/auth/controller.ts': { size: 1024, type: 'typescript' },
@@ -170,12 +170,12 @@ describe('intelQuery', () => {
const result = intelQuery('auth', planningDir);
assert.strictEqual(result.total, 1);
assert.strictEqual(result.matches[0].source, 'files.json');
assert.strictEqual(result.matches[0].source, 'file-roles.json');
assert.strictEqual(result.matches[0].entries[0].key, 'src/auth/controller.ts');
});
test('finds matches in JSON file values', () => {
writeIntelJson(planningDir, 'deps.json', {
writeIntelJson(planningDir, 'dependency-graph.json', {
_meta: { updated_at: new Date().toISOString() },
entries: {
express: { version: '4.18.0', type: 'runtime', used_by: ['src/server.ts'] },
@@ -188,7 +188,7 @@ describe('intelQuery', () => {
});
test('search is case-insensitive', () => {
writeIntelJson(planningDir, 'files.json', {
writeIntelJson(planningDir, 'file-roles.json', {
entries: {
'src/AuthController.ts': { type: 'typescript' },
},
@@ -198,24 +198,25 @@ describe('intelQuery', () => {
assert.strictEqual(result.total, 1);
});
test('finds matches in arch.md text', () => {
writeIntelMd(planningDir, 'arch.md', [
'# Architecture',
'',
'The system uses a layered architecture with REST API endpoints.',
'Authentication is handled by JWT tokens.',
].join('\n'));
test('finds matches in arch-decisions.json entries', () => {
writeIntelJson(planningDir, 'arch-decisions.json', {
_meta: { updated_at: new Date().toISOString() },
entries: {
'jwt-auth': { decision: 'Use JWT tokens for stateless authentication', status: 'accepted' },
'rest-api': { decision: 'REST API endpoints for all services', status: 'accepted' },
},
});
const result = intelQuery('JWT', planningDir);
assert.strictEqual(result.total, 1);
assert.strictEqual(result.matches[0].source, 'arch.md');
assert.strictEqual(result.matches[0].source, 'arch-decisions.json');
});
test('searches across multiple intel files', () => {
writeIntelJson(planningDir, 'files.json', {
writeIntelJson(planningDir, 'file-roles.json', {
entries: { 'src/auth.ts': { exports: ['authenticate'] } },
});
writeIntelJson(planningDir, 'apis.json', {
writeIntelJson(planningDir, 'api-map.json', {
entries: { '/api/auth': { method: 'POST', handler: 'authenticate' } },
});
@@ -244,30 +245,30 @@ describe('intelStatus', () => {
test('reports missing files as stale', () => {
const result = intelStatus(planningDir);
assert.strictEqual(result.overall_stale, true);
assert.strictEqual(result.files['files.json'].exists, false);
assert.strictEqual(result.files['files.json'].stale, true);
assert.strictEqual(result.files['file-roles.json'].exists, false);
assert.strictEqual(result.files['file-roles.json'].stale, true);
});
test('reports fresh files as not stale', () => {
writeIntelJson(planningDir, 'files.json', {
writeIntelJson(planningDir, 'file-roles.json', {
_meta: { updated_at: new Date().toISOString() },
entries: {},
});
const result = intelStatus(planningDir);
assert.strictEqual(result.files['files.json'].exists, true);
assert.strictEqual(result.files['files.json'].stale, false);
assert.strictEqual(result.files['file-roles.json'].exists, true);
assert.strictEqual(result.files['file-roles.json'].stale, false);
});
test('reports old files as stale', () => {
const oldDate = new Date(Date.now() - 25 * 60 * 60 * 1000).toISOString();
writeIntelJson(planningDir, 'files.json', {
writeIntelJson(planningDir, 'file-roles.json', {
_meta: { updated_at: oldDate },
entries: {},
});
const result = intelStatus(planningDir);
assert.strictEqual(result.files['files.json'].stale, true);
assert.strictEqual(result.files['file-roles.json'].stale, true);
assert.strictEqual(result.overall_stale, true);
});
});
@@ -303,24 +304,24 @@ describe('intelDiff', () => {
);
// Add a file after snapshot
writeIntelJson(planningDir, 'files.json', { entries: {} });
writeIntelJson(planningDir, 'file-roles.json', { entries: {} });
const result = intelDiff(planningDir);
assert.ok(result.added.includes('files.json'));
assert.ok(result.added.includes('file-roles.json'));
});
test('detects changed files since snapshot', () => {
// Write initial file
writeIntelJson(planningDir, 'files.json', { entries: { a: 1 } });
writeIntelJson(planningDir, 'file-roles.json', { entries: { a: 1 } });
// Take snapshot
intelSnapshot(planningDir);
// Modify file
writeIntelJson(planningDir, 'files.json', { entries: { a: 1, b: 2 } });
writeIntelJson(planningDir, 'file-roles.json', { entries: { a: 1, b: 2 } });
const result = intelDiff(planningDir);
assert.ok(result.changed.includes('files.json'));
assert.ok(result.changed.includes('file-roles.json'));
});
});
@@ -341,7 +342,7 @@ describe('intelSnapshot', () => {
});
test('saves snapshot with file hashes', () => {
writeIntelJson(planningDir, 'files.json', { entries: {} });
writeIntelJson(planningDir, 'file-roles.json', { entries: {} });
const result = intelSnapshot(planningDir);
assert.strictEqual(result.saved, true);
@@ -351,7 +352,7 @@ describe('intelSnapshot', () => {
const snapshot = JSON.parse(
fs.readFileSync(path.join(planningDir, 'intel', '.last-refresh.json'), 'utf8')
);
assert.ok(snapshot.hashes['files.json']);
assert.ok(snapshot.hashes['file-roles.json']);
});
});
@@ -379,11 +380,11 @@ describe('intelValidate', () => {
});
test('reports warnings for missing _meta.updated_at', () => {
writeIntelJson(planningDir, 'files.json', { entries: {} });
writeIntelJson(planningDir, 'apis.json', { entries: {} });
writeIntelJson(planningDir, 'deps.json', { entries: {} });
writeIntelJson(planningDir, 'file-roles.json', { entries: {} });
writeIntelJson(planningDir, 'api-map.json', { entries: {} });
writeIntelJson(planningDir, 'dependency-graph.json', { entries: {} });
writeIntelJson(planningDir, 'stack.json', { entries: {} });
writeIntelMd(planningDir, 'arch.md', '# Architecture\n');
writeIntelJson(planningDir, 'arch-decisions.json', { entries: {} });
const result = intelValidate(planningDir);
assert.strictEqual(result.valid, true);
@@ -393,11 +394,11 @@ describe('intelValidate', () => {
test('reports invalid JSON as error', () => {
const intelPath = path.join(planningDir, 'intel');
fs.mkdirSync(intelPath, { recursive: true });
fs.writeFileSync(path.join(intelPath, 'files.json'), 'not valid json', 'utf8');
writeIntelJson(planningDir, 'apis.json', { entries: {} });
writeIntelJson(planningDir, 'deps.json', { entries: {} });
fs.writeFileSync(path.join(intelPath, 'file-roles.json'), 'not valid json', 'utf8');
writeIntelJson(planningDir, 'api-map.json', { entries: {} });
writeIntelJson(planningDir, 'dependency-graph.json', { entries: {} });
writeIntelJson(planningDir, 'stack.json', { entries: {} });
writeIntelMd(planningDir, 'arch.md', '# Architecture\n');
writeIntelJson(planningDir, 'arch-decisions.json', { entries: {} });
const result = intelValidate(planningDir);
assert.strictEqual(result.valid, false);
@@ -406,15 +407,15 @@ describe('intelValidate', () => {
test('passes validation with complete fresh intel', () => {
const now = new Date().toISOString();
writeIntelJson(planningDir, 'files.json', {
writeIntelJson(planningDir, 'file-roles.json', {
_meta: { updated_at: now },
entries: {},
});
writeIntelJson(planningDir, 'apis.json', {
writeIntelJson(planningDir, 'api-map.json', {
_meta: { updated_at: now },
entries: {},
});
writeIntelJson(planningDir, 'deps.json', {
writeIntelJson(planningDir, 'dependency-graph.json', {
_meta: { updated_at: now },
entries: {},
});
@@ -422,7 +423,10 @@ describe('intelValidate', () => {
_meta: { updated_at: now },
entries: {},
});
writeIntelMd(planningDir, 'arch.md', '# Architecture\n');
writeIntelJson(planningDir, 'arch-decisions.json', {
_meta: { updated_at: now },
entries: {},
});
const result = intelValidate(planningDir);
assert.strictEqual(result.valid, true);
@@ -446,12 +450,12 @@ describe('intelPatchMeta', () => {
});
test('patches _meta.updated_at and increments version', () => {
writeIntelJson(planningDir, 'files.json', {
writeIntelJson(planningDir, 'file-roles.json', {
_meta: { updated_at: '2025-01-01T00:00:00Z', version: 1 },
entries: {},
});
const filePath = path.join(planningDir, 'intel', 'files.json');
const filePath = path.join(planningDir, 'intel', 'file-roles.json');
const result = intelPatchMeta(filePath);
assert.strictEqual(result.patched, true);
@@ -462,9 +466,9 @@ describe('intelPatchMeta', () => {
});
test('creates _meta if missing', () => {
writeIntelJson(planningDir, 'files.json', { entries: {} });
writeIntelJson(planningDir, 'file-roles.json', { entries: {} });
const filePath = path.join(planningDir, 'intel', 'files.json');
const filePath = path.join(planningDir, 'intel', 'file-roles.json');
const result = intelPatchMeta(filePath);
assert.strictEqual(result.patched, true);