Skip to content

Commit 6a52c0d

Browse files
committed
feat(06-01): gate index consumers on IndexMeta validation
- Validate index-meta + artifacts before serving any index-derived data - Fail closed on legacy/corrupt keyword index to trigger auto-heal - Attach index status/confidence/action metadata to index-consuming responses
1 parent a216c6d commit 6a52c0d

File tree

9 files changed

+632
-55
lines changed

9 files changed

+632
-55
lines changed

src/analyzers/angular/index.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -885,11 +885,17 @@ export class AngularAnalyzer implements FrameworkAnalyzer {
885885
try {
886886
const indexPath = path.join(rootPath, CODEBASE_CONTEXT_DIRNAME, KEYWORD_INDEX_FILENAME);
887887
const indexContent = await fs.readFile(indexPath, 'utf-8');
888-
const chunks = JSON.parse(indexContent);
888+
const parsed = JSON.parse(indexContent) as any;
889889

890-
console.error(`Loading statistics from ${indexPath}: ${chunks.length} chunks`);
890+
// Legacy index.json is an array — do not consume it (missing version/meta headers).
891+
if (Array.isArray(parsed)) {
892+
return metadata;
893+
}
891894

895+
const chunks = parsed && Array.isArray(parsed.chunks) ? parsed.chunks : null;
892896
if (Array.isArray(chunks) && chunks.length > 0) {
897+
console.error(`Loading statistics from ${indexPath}: ${chunks.length} chunks`);
898+
893899
metadata.statistics.totalFiles = new Set(chunks.map((c: any) => c.filePath)).size;
894900
metadata.statistics.totalLines = chunks.reduce(
895901
(sum: number, c: any) => sum + (c.endLine - c.startLine + 1),

src/core/indexer.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -873,7 +873,12 @@ export class CodebaseIndexer {
873873
INTELLIGENCE_FILENAME
874874
);
875875
const intelligenceContent = await fs.readFile(intelligencePath, 'utf-8');
876-
const intelligence = JSON.parse(intelligenceContent);
876+
const intelligence = JSON.parse(intelligenceContent) as any;
877+
878+
// Phase 06: ignore legacy intelligence files that lack a versioned header.
879+
if (!intelligence || typeof intelligence !== 'object' || !intelligence.header) {
880+
return metadata;
881+
}
877882

878883
metadata.customMetadata = {
879884
...metadata.customMetadata,

src/core/search.ts

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import { IndexCorruptedError } from '../errors/index.js';
1414
import { isTestingRelatedQuery } from '../preflight/query-scope.js';
1515
import { assessSearchQuality } from './search-quality.js';
1616
import { rerank } from './reranker.js';
17+
import { type IndexMeta, readIndexMeta, validateIndexArtifacts } from './index-meta.js';
1718
import {
1819
CODEBASE_CONTEXT_DIRNAME,
1920
INTELLIGENCE_FILENAME,
@@ -112,6 +113,8 @@ export class CodebaseSearcher {
112113
private rootPath: string;
113114
private storagePath: string;
114115

116+
private indexMeta: IndexMeta | null = null;
117+
115118
private fuseIndex: Fuse<CodeChunk> | null = null;
116119
private chunks: CodeChunk[] = [];
117120

@@ -138,6 +141,10 @@ export class CodebaseSearcher {
138141
if (this.initialized) return;
139142

140143
try {
144+
// Fail closed on version mismatch/corruption before serving any results.
145+
this.indexMeta = await readIndexMeta(this.rootPath);
146+
await validateIndexArtifacts(this.rootPath, this.indexMeta);
147+
141148
await this.loadKeywordIndex();
142149
await this.loadPatternIntelligence();
143150

@@ -160,7 +167,20 @@ export class CodebaseSearcher {
160167
try {
161168
const indexPath = path.join(this.rootPath, CODEBASE_CONTEXT_DIRNAME, KEYWORD_INDEX_FILENAME);
162169
const content = await fs.readFile(indexPath, 'utf-8');
163-
this.chunks = JSON.parse(content);
170+
const parsed = JSON.parse(content) as any;
171+
172+
if (Array.isArray(parsed)) {
173+
throw new IndexCorruptedError(
174+
'Legacy keyword index format detected (missing header). Rebuild required.'
175+
);
176+
}
177+
178+
const chunks = parsed && Array.isArray(parsed.chunks) ? parsed.chunks : null;
179+
if (!chunks) {
180+
throw new IndexCorruptedError('Keyword index corrupted: expected { header, chunks }');
181+
}
182+
183+
this.chunks = chunks;
164184

165185
this.fuseIndex = new Fuse(this.chunks, {
166186
keys: [
@@ -178,9 +198,12 @@ export class CodebaseSearcher {
178198
ignoreLocation: true
179199
});
180200
} catch (error) {
181-
console.warn('Keyword index load failed:', error);
182-
this.chunks = [];
183-
this.fuseIndex = null;
201+
if (error instanceof IndexCorruptedError) {
202+
throw error;
203+
}
204+
throw new IndexCorruptedError(
205+
`Keyword index load failed (rebuild required): ${error instanceof Error ? error.message : String(error)}`
206+
);
184207
}
185208
}
186209

src/core/symbol-references.ts

Lines changed: 21 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { promises as fs } from 'fs';
22
import path from 'path';
33
import { CODEBASE_CONTEXT_DIRNAME, KEYWORD_INDEX_FILENAME } from '../constants/codebase-context.js';
4+
import { IndexCorruptedError } from '../errors/index.js';
45

56
interface IndexedChunk {
67
content?: unknown;
@@ -78,18 +79,27 @@ export async function findSymbolReferences(
7879
try {
7980
const content = await fs.readFile(indexPath, 'utf-8');
8081
chunksRaw = JSON.parse(content);
81-
} catch {
82-
return {
83-
status: 'error',
84-
message: 'Run indexing first'
85-
};
82+
} catch (error) {
83+
throw new IndexCorruptedError(
84+
`Keyword index missing or unreadable (rebuild required): ${
85+
error instanceof Error ? error.message : String(error)
86+
}`
87+
);
8688
}
8789

88-
if (!Array.isArray(chunksRaw)) {
89-
return {
90-
status: 'error',
91-
message: 'Run indexing first'
92-
};
90+
if (Array.isArray(chunksRaw)) {
91+
throw new IndexCorruptedError(
92+
'Legacy keyword index format detected (missing header). Rebuild required.'
93+
);
94+
}
95+
96+
const chunks =
97+
chunksRaw && typeof chunksRaw === 'object' && Array.isArray((chunksRaw as any).chunks)
98+
? ((chunksRaw as any).chunks as unknown[])
99+
: null;
100+
101+
if (!chunks) {
102+
throw new IndexCorruptedError('Keyword index corrupted: expected { header, chunks }');
93103
}
94104

95105
const usages: SymbolUsage[] = [];
@@ -98,7 +108,7 @@ export async function findSymbolReferences(
98108
const escapedSymbol = escapeRegex(normalizedSymbol);
99109
const matcher = new RegExp(`\\b${escapedSymbol}\\b`, 'g');
100110

101-
for (const chunkRaw of chunksRaw) {
111+
for (const chunkRaw of chunks) {
102112
const chunk = chunkRaw as IndexedChunk;
103113
if (typeof chunk.content !== 'string') {
104114
continue;

0 commit comments

Comments
 (0)