Skip to content

Commit 468b2bf

Browse files
committed
feat(core): add comprehensive Zod validation and change frequency tracking
- Create comprehensive Zod schemas for all indexer stats types - Add runtime validation for state loading/saving - Implement change frequency tracking for dashboard - Add avgCommitsPerFile and lastModified to LanguageStats - Add totalCommits and lastModified to PackageStats - 52 new tests (schemas + change frequency) - All 577 core tests passing Follows TYPESCRIPT_STANDARDS.md - eliminates type assertions with runtime validation at external boundaries (state files). Aligns with Dashboard & Visualization epic.
1 parent 989f5d1 commit 468b2bf

File tree

11 files changed

+1669
-5
lines changed

11 files changed

+1669
-5
lines changed

packages/core/src/indexer/index.ts

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import type { Document } from '../scanner/types';
1010
import { getCurrentSystemResources, getOptimalConcurrency } from '../utils/concurrency';
1111
import { VectorStorage } from '../vector';
1212
import type { EmbeddingDocument, SearchOptions, SearchResult } from '../vector/types';
13+
import { validateDetailedIndexStats, validateIndexerState } from './schemas/validation.js';
1314
import { StatsAggregator } from './stats-aggregator';
1415
import { mergeStats } from './stats-merger';
1516
import type {
@@ -450,7 +451,7 @@ export class RepositoryIndexer {
450451
const incrementalUpdatesSince = this.state.incrementalUpdatesSince || 0;
451452
const warning = this.getStatsWarning(incrementalUpdatesSince);
452453

453-
return {
454+
const stats = {
454455
filesScanned: this.state.stats.totalFiles,
455456
documentsExtracted: this.state.stats.totalDocuments,
456457
documentsIndexed: this.state.stats.totalDocuments,
@@ -471,6 +472,15 @@ export class RepositoryIndexer {
471472
warning,
472473
},
473474
};
475+
476+
// Validate stats before returning (ensures API contract)
477+
const validation = validateDetailedIndexStats(stats);
478+
if (!validation.success) {
479+
console.warn(`Invalid stats detected: ${validation.error}`);
480+
return null;
481+
}
482+
483+
return validation.data;
474484
}
475485

476486
/**
@@ -548,10 +558,20 @@ export class RepositoryIndexer {
548558
private async loadState(): Promise<void> {
549559
try {
550560
const stateContent = await fs.readFile(this.config.statePath, 'utf-8');
551-
this.state = JSON.parse(stateContent);
561+
const data = JSON.parse(stateContent);
562+
563+
// Validate state with Zod schema
564+
const validation = validateIndexerState(data);
565+
if (!validation.success) {
566+
console.warn(`Invalid indexer state (will start fresh): ${validation.error}`);
567+
this.state = null;
568+
return;
569+
}
570+
571+
this.state = validation.data;
552572

553573
// Validate state compatibility
554-
if (this.state && this.state.version !== INDEXER_VERSION) {
574+
if (this.state.version !== INDEXER_VERSION) {
555575
console.warn(
556576
`Indexer state version mismatch: ${this.state.version} vs ${INDEXER_VERSION}. May need re-indexing.`
557577
);
@@ -570,6 +590,13 @@ export class RepositoryIndexer {
570590
return;
571591
}
572592

593+
// Validate state before saving (defensive check)
594+
const validation = validateIndexerState(this.state);
595+
if (!validation.success) {
596+
// Log warning but don't block saving - state was valid when created
597+
console.warn(`Indexer state validation warning: ${validation.error}`);
598+
}
599+
573600
// Ensure directory exists
574601
await fs.mkdir(path.dirname(this.config.statePath), { recursive: true });
575602

0 commit comments

Comments
 (0)