@claude-vector/core
Version:
Core vector search engine for code intelligence
979 lines (826 loc) • 31.4 kB
JavaScript
/**
* Incremental Indexer - リアルタイム増分インデックス更新システム
*
* 機能:
* - ファイル変更の監視とリアルタイム更新
* - 影響分析による効率的な部分更新
* - 依存関係グラフの動的更新
* - 差分ベースのエンベディング更新
*/
import fs from 'fs/promises';
import fsSync from 'fs';
import path from 'path';
import { watch } from 'fs';
import { EventEmitter } from 'events';
import crypto from 'crypto';
import { SemanticChunker } from './semantic-chunker.js';
import { AIMetadataManager } from './ai-metadata-manager.js';
import { VectorSearchEngine } from './engine.js';
export class IncrementalIndexer extends EventEmitter {
constructor(options = {}) {
super();
this.options = {
watchPatterns: options.watchPatterns || ['**/*.{js,jsx,ts,tsx,md}'],
ignorePatterns: options.ignorePatterns || ['node_modules/**', '.git/**', 'dist/**'],
debounceMs: options.debounceMs || 500,
batchSize: options.batchSize || 10,
enableRealtime: options.enableRealtime ?? true,
indexPath: options.indexPath || '.claude-code-index',
backupEnabled: options.backupEnabled ?? true,
...options
};
this.semanticChunker = new SemanticChunker();
this.metadataManager = new AIMetadataManager();
this.vectorEngine = new VectorSearchEngine();
this.watchers = new Map();
this.changeQueue = [];
this.processingQueue = false;
this.debounceTimers = new Map();
this.dependencyGraph = new Map();
this.indexData = {
embeddings: [],
chunks: [],
metadata: {},
relationships: new Map()
};
this.isInitialized = false;
}
/**
* 増分インデクサーの初期化
*/
async initialize(projectRoot) {
this.projectRoot = projectRoot;
this.indexPath = path.join(projectRoot, this.options.indexPath);
// 既存インデックスの読み込み
await this.loadExistingIndex();
// 依存関係グラフの構築
await this.buildDependencyGraph();
// ファイル監視の開始
if (this.options.enableRealtime) {
await this.startWatching();
}
this.isInitialized = true;
this.emit('initialized', { projectRoot, indexPath: this.indexPath });
console.log('✅ Incremental Indexer initialized');
}
/**
* 既存インデックスの読み込み
*/
async loadExistingIndex() {
try {
const embeddingsPath = path.join(this.indexPath, 'embeddings.json');
const chunksPath = path.join(this.indexPath, 'chunks.json');
const metadataPath = path.join(this.indexPath, 'metadata.json');
if (fsSync.existsSync(embeddingsPath) && fsSync.existsSync(chunksPath)) {
console.log('📂 Loading existing index...');
const [embeddingsData, chunksData, metadataData] = await Promise.all([
fs.readFile(embeddingsPath, 'utf-8'),
fs.readFile(chunksPath, 'utf-8'),
fsSync.existsSync(metadataPath) ? fs.readFile(metadataPath, 'utf-8') : Promise.resolve('{}')
]);
this.indexData.embeddings = JSON.parse(embeddingsData);
this.indexData.chunks = JSON.parse(chunksData);
this.indexData.metadata = JSON.parse(metadataData);
console.log(`📊 Loaded ${this.indexData.chunks.length} existing chunks`);
} else {
console.log('📝 No existing index found, starting fresh');
}
} catch (error) {
console.warn('⚠️ Failed to load existing index:', error.message);
// エラーの場合は空のインデックスから開始
this.indexData = {
embeddings: [],
chunks: [],
metadata: {},
relationships: new Map()
};
}
}
/**
* 依存関係グラフの構築
*/
async buildDependencyGraph() {
console.log('🔗 Building dependency graph...');
for (const chunk of this.indexData.chunks) {
if (chunk.metadata && chunk.metadata.relationships) {
const chunkId = chunk.metadata.id || this.generateChunkId(chunk.metadata.file, chunk.metadata.startLine);
// 依存関係の追加
if (chunk.metadata.relationships.dependencies) {
chunk.metadata.relationships.dependencies.forEach(dep => {
if (!this.dependencyGraph.has(dep)) {
this.dependencyGraph.set(dep, new Set());
}
this.dependencyGraph.get(dep).add(chunkId);
});
}
}
}
console.log(`🔗 Built dependency graph with ${this.dependencyGraph.size} nodes`);
}
/**
* ファイル監視の開始
*/
async startWatching() {
console.log('👀 Starting file watching...');
const watchOptions = {
recursive: true,
persistent: true
};
try {
const watcher = watch(this.projectRoot, watchOptions, (eventType, filename) => {
if (filename && this.shouldWatchFile(filename)) {
this.handleFileChange(eventType, filename);
}
});
this.watchers.set(this.projectRoot, watcher);
console.log('👀 File watching started');
} catch (error) {
console.warn('⚠️ Failed to start file watching:', error.message);
}
}
/**
* ファイル監視対象判定
*/
shouldWatchFile(filename) {
// 無視パターンのチェック
for (const pattern of this.options.ignorePatterns) {
if (this.matchPattern(filename, pattern)) {
return false;
}
}
// 監視パターンのチェック
for (const pattern of this.options.watchPatterns) {
if (this.matchPattern(filename, pattern)) {
return true;
}
}
return false;
}
/**
* 簡単なパターンマッチング
*/
matchPattern(filename, pattern) {
// 簡略化されたglob風パターンマッチング
const regexPattern = pattern
.replace(/\*\*/g, '.*')
.replace(/\*/g, '[^/]*')
.replace(/\./g, '\\.');
const regex = new RegExp(regexPattern);
return regex.test(filename);
}
/**
* ファイル変更の処理
*/
handleFileChange(eventType, filename) {
const fullPath = path.join(this.projectRoot, filename);
// デバウンス処理
if (this.debounceTimers.has(fullPath)) {
clearTimeout(this.debounceTimers.get(fullPath));
}
const timer = setTimeout(() => {
this.queueFileUpdate(eventType, fullPath);
this.debounceTimers.delete(fullPath);
}, this.options.debounceMs);
this.debounceTimers.set(fullPath, timer);
}
/**
* ファイル更新のキューイング
*/
queueFileUpdate(eventType, filePath) {
this.changeQueue.push({
eventType,
filePath,
timestamp: Date.now()
});
this.emit('file-changed', { eventType, filePath });
// キューの処理開始
if (!this.processingQueue) {
this.processChangeQueue();
}
}
/**
* 変更キューの処理
*/
async processChangeQueue() {
if (this.processingQueue || this.changeQueue.length === 0) {
return;
}
this.processingQueue = true;
try {
// バッチ処理
const batch = this.changeQueue.splice(0, this.options.batchSize);
console.log(`🔄 Processing ${batch.length} file changes...`);
for (const change of batch) {
await this.processFileChange(change);
}
// インデックスの保存
await this.saveIndex();
this.emit('batch-processed', { count: batch.length });
} catch (error) {
console.error('❌ Error processing change queue:', error);
this.emit('error', error);
} finally {
this.processingQueue = false;
// 残りのキューがある場合は続行
if (this.changeQueue.length > 0) {
setTimeout(() => this.processChangeQueue(), 100);
}
}
}
/**
* 個別ファイル変更の処理
*/
async processFileChange(change) {
const { eventType, filePath } = change;
console.log(`📝 Processing ${eventType}: ${path.relative(this.projectRoot, filePath)}`);
switch (eventType) {
case 'change':
await this.updateFile(filePath);
break;
case 'rename':
// ファイルの存在確認
if (fsSync.existsSync(filePath)) {
await this.addFile(filePath);
} else {
await this.removeFile(filePath);
}
break;
default:
console.log(`🔄 Unhandled event type: ${eventType}`);
}
}
/**
* ファイルの更新(真の差分更新)
*/
async updateFile(filePath) {
try {
const content = await fs.readFile(filePath, 'utf-8');
// ファイル統計情報を取得
const stats = await fs.stat(filePath);
const fileStats = {
lastModified: stats.mtime.getTime(),
size: stats.size,
checksum: crypto.createHash('md5').update(content).digest('hex')
};
// 既存チャンクの取得(削除しない)
const existingChunks = this.indexData.chunks.filter(chunk =>
chunk.metadata.file === filePath
);
const existingIndices = this.indexData.chunks
.map((chunk, index) => ({ chunk, index }))
.filter(item => item.chunk.metadata.file === filePath)
.map(item => item.index);
// 新しいチャンクの作成
const newChunks = await this.semanticChunker.chunkFile(filePath, content);
// チャンクの差分比較
const { added, modified, unchanged } = this.compareChunks(existingChunks, newChunks);
let addedCount = 0;
let modifiedCount = 0;
let reusedCount = 0;
let removedCount = 0;
// 使用済みインデックスの追跡
const usedIndices = new Set();
// 変更されていないチャンクは再利用
for (const { existing, updated } of unchanged) {
const index = existingIndices.find(i =>
this.isSameChunk(this.indexData.chunks[i], existing)
);
if (index !== undefined) {
// メタデータのみ更新(エンベディングは再利用)
this.indexData.chunks[index] = await this.createEnhancedChunk(updated, filePath, fileStats);
usedIndices.add(index);
reusedCount++;
}
}
// 変更されたチャンクの更新
for (const { existing, updated } of modified) {
const index = existingIndices.find(i =>
this.isSameChunk(this.indexData.chunks[i], existing)
);
if (index !== undefined) {
// チャンクとエンベディングの更新
const enhancedChunk = await this.createEnhancedChunk(updated, filePath, fileStats);
const embedding = await this.embeddingGenerator.generateEmbedding(updated.content);
this.indexData.chunks[index] = enhancedChunk;
this.indexData.embeddings[index] = embedding;
usedIndices.add(index);
modifiedCount++;
}
}
// 新しいチャンクの追加
for (const chunk of added) {
const enhancedChunk = await this.createEnhancedChunk(chunk, filePath, fileStats);
const embedding = await this.embeddingGenerator.generateEmbedding(chunk.content);
this.indexData.chunks.push(enhancedChunk);
this.indexData.embeddings.push(embedding);
addedCount++;
}
// 使用されなかった既存チャンクを削除(逆順で)
const toRemove = existingIndices.filter(i => !usedIndices.has(i)).sort((a, b) => b - a);
for (const index of toRemove) {
this.indexData.chunks.splice(index, 1);
this.indexData.embeddings.splice(index, 1);
removedCount++;
}
// 依存関係グラフの更新
await this.updateDependencyGraphAfterRemoval(filePath);
console.log(`✅ Updated: ${path.relative(this.projectRoot, filePath)}`);
console.log(` 📊 Changes: +${addedCount} added, ~${modifiedCount} modified, =${reusedCount} reused, -${removedCount} removed`);
} catch (error) {
console.error(`❌ Failed to update file ${filePath}:`, error.message);
}
}
/**
* ファイルの追加
*/
async addFile(filePath) {
try {
const content = await fs.readFile(filePath, 'utf-8');
// ファイル統計情報を取得
const stats = await fs.stat(filePath);
const fileStats = {
lastModified: stats.mtime.getTime(),
size: stats.size,
checksum: crypto.createHash('md5').update(content).digest('hex')
};
// 既存チャンクの検索
const existingChunks = this.indexData.chunks.filter(chunk =>
chunk.metadata.file === filePath
);
// 新しいチャンクの作成
const newChunks = await this.semanticChunker.chunkFile(filePath, content);
// チャンクの差分比較
const { added, modified, unchanged } = this.compareChunks(existingChunks, newChunks);
let addedCount = 0;
let modifiedCount = 0;
let reusedCount = 0;
// 既存の不要なチャンクを削除
if (existingChunks.length > 0) {
const toRemove = existingChunks.filter(existing =>
!unchanged.some(u => this.isSameChunk(u.existing, existing)) &&
!modified.some(m => this.isSameChunk(m.existing, existing))
);
for (const chunk of toRemove) {
const index = this.indexData.chunks.findIndex(c =>
this.isSameChunk(c, chunk)
);
if (index !== -1) {
this.indexData.chunks.splice(index, 1);
this.indexData.embeddings.splice(index, 1);
}
}
}
// 新しいチャンクの追加
for (const chunk of added) {
const enhancedChunk = await this.createEnhancedChunk(chunk, filePath, fileStats);
const embedding = await this.vectorEngine.generateQueryEmbedding(enhancedChunk.content);
this.indexData.chunks.push(enhancedChunk);
this.indexData.embeddings.push(embedding);
addedCount++;
}
// 変更されたチャンクの更新
for (const { existing, updated } of modified) {
const index = this.indexData.chunks.findIndex(c =>
this.isSameChunk(c, existing)
);
if (index !== -1) {
const enhancedChunk = await this.createEnhancedChunk(updated, filePath, fileStats);
const embedding = await this.vectorEngine.generateQueryEmbedding(enhancedChunk.content);
this.indexData.chunks[index] = enhancedChunk;
this.indexData.embeddings[index] = embedding;
modifiedCount++;
}
}
// 変更されていないチャンクは再利用
reusedCount = unchanged.length;
// 依存関係グラフの更新
for (const chunk of [...added, ...modified.map(m => m.updated)]) {
await this.updateDependencyGraph(chunk);
}
const totalProcessed = addedCount + modifiedCount + reusedCount;
// ログ出力の改善
if (addedCount > 0 || modifiedCount > 0) {
console.log(`✅ Added ${addedCount} chunks from: ${path.relative(this.projectRoot, filePath)}`);
if (process.env.DEBUG) {
console.log(` 📊 Details: ${addedCount} new, ${modifiedCount} modified, ${reusedCount} reused`);
}
} else {
// スキップされた場合はデバッグモードでのみ表示
if (process.env.DEBUG) {
console.log(`✅ Skipped: ${path.relative(this.projectRoot, filePath)} (no changes, ${reusedCount} reused)`);
}
}
} catch (error) {
console.error(`❌ Failed to add file ${filePath}:`, error.message);
}
}
/**
* チャンクの差分比較
*/
compareChunks(existingChunks, newChunks) {
const added = [];
const modified = [];
const unchanged = [];
const usedExisting = new Set();
// 新しいチャンクとの比較
for (const newChunk of newChunks) {
const newContentHash = this.calculateContentHash(newChunk.content);
let matched = false;
// 既存チャンクから完全一致を検索
for (const [index, existing] of existingChunks.entries()) {
if (usedExisting.has(index)) continue;
const existingContentHash = this.calculateContentHash(existing.content);
if (newContentHash === existingContentHash) {
// コンテンツが同じ場合は未変更
unchanged.push({ existing, updated: newChunk });
usedExisting.add(index);
matched = true;
break;
}
}
if (!matched) {
// 類似チャンクを検索(位置ベース)
for (const [index, existing] of existingChunks.entries()) {
if (usedExisting.has(index)) continue;
if (this.isSimilarPosition(existing, newChunk)) {
modified.push({ existing, updated: newChunk });
usedExisting.add(index);
matched = true;
break;
}
}
if (!matched) {
added.push(newChunk);
}
}
}
return { added, modified, unchanged };
}
/**
* 位置ベースの類似性判定
*/
isSimilarPosition(chunk1, chunk2) {
if (!chunk1.metadata || !chunk2.metadata) return false;
// 分割チャンクの場合、splitIndexも考慮
if (chunk1.metadata.split && chunk2.metadata.split) {
// 同じsplitIndexの場合は対応するチャンク
if (chunk1.metadata.splitIndex === chunk2.metadata.splitIndex) {
return true;
}
}
// 同じ範囲
if (chunk1.metadata.startLine === chunk2.metadata.startLine &&
chunk1.metadata.endLine === chunk2.metadata.endLine) {
return true;
}
// 重複する範囲(70%以上の重複を持つ場合)
if (this.hasSignificantOverlap(chunk1.metadata, chunk2.metadata)) {
return true;
}
return false;
}
/**
* 有意な重複の判定(70%以上)
*/
hasSignificantOverlap(meta1, meta2) {
const overlapStart = Math.max(meta1.startLine, meta2.startLine);
const overlapEnd = Math.min(meta1.endLine, meta2.endLine);
if (overlapStart > overlapEnd) return false;
const overlapLines = overlapEnd - overlapStart + 1;
const chunk1Lines = meta1.endLine - meta1.startLine + 1;
const chunk2Lines = meta2.endLine - meta2.startLine + 1;
// どちらかのチャンクの70%以上が重複している場合
return (overlapLines / chunk1Lines >= 0.7) || (overlapLines / chunk2Lines >= 0.7);
}
/**
* コンテンツハッシュの計算
*/
calculateContentHash(content) {
return crypto.createHash('md5').update(content).digest('hex');
}
/**
* チャンクの重複判定
*/
hasOverlap(meta1, meta2) {
return Math.max(meta1.startLine, meta2.startLine) <= Math.min(meta1.endLine, meta2.endLine);
}
/**
* チャンクの同一性判定
*/
isSameChunk(chunk1, chunk2) {
// chunkIdが両方にある場合はそれを使用
if (chunk1.metadata && chunk1.metadata.chunkId &&
chunk2.metadata && chunk2.metadata.chunkId) {
return chunk1.metadata.chunkId === chunk2.metadata.chunkId;
}
// ファイルパスと行番号による比較
if (chunk1.metadata && chunk2.metadata) {
return chunk1.metadata.file === chunk2.metadata.file &&
chunk1.metadata.startLine === chunk2.metadata.startLine &&
chunk1.metadata.endLine === chunk2.metadata.endLine;
}
// フォールバック: コンテンツハッシュによる比較
return this.calculateContentHash(chunk1.content) === this.calculateContentHash(chunk2.content);
}
/**
* 拡張チャンクの作成
*/
async createEnhancedChunk(chunk, filePath, fileStats) {
const aiMetadata = await this.metadataManager.generateAIMetadata(
chunk,
filePath,
{ phase: 'implementation' }
);
return {
...chunk,
metadata: {
...chunk.metadata,
...aiMetadata,
addedAt: new Date().toISOString(),
// ファイル統計情報を追加(FileChangeDetectorが必要とする)
lastModified: fileStats.lastModified,
size: fileStats.size,
checksum: fileStats.checksum,
fileSize: fileStats.size, // 追加のファイルサイズフィールド
fileChecksum: fileStats.checksum // 追加のチェックサムフィールド
},
// 開発情報にも追加(フォールバック用)
development: {
...(chunk.development || {}),
lastModified: fileStats.lastModified,
fileSize: fileStats.size,
fileChecksum: fileStats.checksum
}
};
}
/**
* ファイルの削除
*/
async removeFile(filePath) {
const removedIndices = [];
// 該当ファイルのチャンクを特定
for (let i = this.indexData.chunks.length - 1; i >= 0; i--) {
if (this.indexData.chunks[i].metadata.file === filePath) {
removedIndices.push(i);
}
}
// 逆順で削除(インデックスの整合性を保つため)
for (const index of removedIndices) {
this.indexData.chunks.splice(index, 1);
this.indexData.embeddings.splice(index, 1);
}
// 依存関係グラフの更新
await this.updateDependencyGraphAfterRemoval(filePath);
console.log(`🗑️ Removed ${removedIndices.length} chunks from: ${path.relative(this.projectRoot, filePath)}`);
}
/**
* 依存関係グラフの更新
*/
async updateDependencyGraph(chunk) {
if (!chunk.metadata.relationships) return;
const chunkId = chunk.metadata.id;
// 依存関係の追加
if (chunk.metadata.relationships.dependencies) {
chunk.metadata.relationships.dependencies.forEach(dep => {
if (!this.dependencyGraph.has(dep)) {
this.dependencyGraph.set(dep, new Set());
}
this.dependencyGraph.get(dep).add(chunkId);
});
}
}
/**
* 削除後の依存関係グラフ更新
*/
async updateDependencyGraphAfterRemoval(filePath) {
// 削除されたファイルに関連する依存関係を清理
for (const [key, dependents] of this.dependencyGraph.entries()) {
const toRemove = [];
dependents.forEach(chunkId => {
// チャンクIDからファイルパスを特定し、削除されたファイルのものかチェック
const chunk = this.indexData.chunks.find(c => c.metadata.id === chunkId);
if (!chunk || chunk.metadata.file === filePath) {
toRemove.push(chunkId);
}
});
toRemove.forEach(chunkId => dependents.delete(chunkId));
if (dependents.size === 0) {
this.dependencyGraph.delete(key);
}
}
}
/**
* 影響分析
*/
async analyzeImpact(filePath) {
const impactedChunks = new Set();
// 直接的な影響(同じファイル)
this.indexData.chunks.forEach((chunk, index) => {
if (chunk.metadata.file === filePath) {
impactedChunks.add(index);
}
});
// 間接的な影響(依存関係)
for (const [dep, dependents] of this.dependencyGraph.entries()) {
// ファイルパスから依存関係を特定
if (dep.includes(path.basename(filePath, path.extname(filePath)))) {
dependents.forEach(chunkId => {
const chunkIndex = this.indexData.chunks.findIndex(c => c.metadata.id === chunkId);
if (chunkIndex !== -1) {
impactedChunks.add(chunkIndex);
}
});
}
}
return {
directImpact: Array.from(impactedChunks).filter(index =>
this.indexData.chunks[index].metadata.file === filePath
),
indirectImpact: Array.from(impactedChunks).filter(index =>
this.indexData.chunks[index].metadata.file !== filePath
),
totalImpacted: impactedChunks.size
};
}
/**
* インデックスの保存
*/
async saveIndex() {
try {
// バックアップの作成
if (this.options.backupEnabled) {
await this.createBackup();
}
// インデックスディレクトリの作成
await fs.mkdir(this.indexPath, { recursive: true });
// 並列保存
await Promise.all([
fs.writeFile(
path.join(this.indexPath, 'embeddings.json'),
JSON.stringify(this.indexData.embeddings)
),
fs.writeFile(
path.join(this.indexPath, 'chunks.json'),
JSON.stringify(this.indexData.chunks)
),
fs.writeFile(
path.join(this.indexPath, 'metadata.json'),
JSON.stringify({
createdAt: this.indexData.metadata.createdAt || new Date().toISOString(),
updatedAt: new Date().toISOString(),
totalChunks: this.indexData.chunks.length,
totalFiles: [...new Set(this.indexData.chunks.map(c => c.metadata.file))].length,
incrementalUpdate: true,
version: '2.0.0',
// ファイルレベルのメタデータを追加
files: this.buildFileMetadata()
}, null, 2)
)
]);
this.emit('index-saved', {
chunks: this.indexData.chunks.length,
path: this.indexPath
});
} catch (error) {
console.error('❌ Failed to save index:', error);
throw error;
}
}
/**
* ファイルレベルのメタデータ構築
*/
buildFileMetadata() {
const fileMetadata = {};
// チャンクからファイルレベルのメタデータを構築
for (const chunk of this.indexData.chunks) {
if (chunk.metadata && chunk.metadata.file) {
const filePath = chunk.metadata.file;
// 実際のチャンクデータ構造に基づいて情報を抽出
const lastModified = chunk.metadata.lastModified ||
(chunk.development && chunk.development.lastModified) ||
(chunk.metadata.timestamp && new Date(chunk.metadata.timestamp).getTime()) ||
null;
const size = chunk.metadata.size ||
chunk.metadata.fileSize ||
(chunk.development && chunk.development.fileSize) ||
null;
const checksum = chunk.metadata.checksum ||
chunk.metadata.fileChecksum ||
(chunk.development && chunk.development.fileChecksum) ||
null;
// ファイルの最新情報を取得(複数のチャンクから同じファイルの場合は最新の情報を使用)
if (!fileMetadata[filePath]) {
fileMetadata[filePath] = {
lastModified: lastModified,
size: size,
checksum: checksum,
chunkCount: 0
};
} else if (lastModified && (!fileMetadata[filePath].lastModified ||
(typeof lastModified === 'string' ? new Date(lastModified).getTime() : lastModified) >
(typeof fileMetadata[filePath].lastModified === 'string' ? new Date(fileMetadata[filePath].lastModified).getTime() : fileMetadata[filePath].lastModified))) {
// より新しい情報が利用可能な場合に更新
fileMetadata[filePath].lastModified = lastModified;
fileMetadata[filePath].size = size || fileMetadata[filePath].size;
fileMetadata[filePath].checksum = checksum || fileMetadata[filePath].checksum;
}
}
}
// チャンク数をカウント
for (const chunk of this.indexData.chunks) {
if (chunk.metadata && chunk.metadata.file && fileMetadata[chunk.metadata.file]) {
fileMetadata[chunk.metadata.file].chunkCount++;
}
}
// 情報が不足しているファイルの統計
const filesToUpdate = [];
for (const [filePath, metadata] of Object.entries(fileMetadata)) {
if (!metadata.lastModified || !metadata.checksum) {
filesToUpdate.push(filePath);
}
}
if (filesToUpdate.length > 0) {
console.log(`⚠️ Warning: ${filesToUpdate.length} files missing complete metadata (lastModified: ${Object.values(fileMetadata).filter(f => f.lastModified).length}/${Object.keys(fileMetadata).length}, checksum: ${Object.values(fileMetadata).filter(f => f.checksum).length}/${Object.keys(fileMetadata).length})`);
}
return fileMetadata;
}
/**
* バックアップの作成
*/
async createBackup() {
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
const backupDir = path.join(this.indexPath, 'backups', timestamp);
try {
await fs.mkdir(backupDir, { recursive: true });
const files = ['embeddings.json', 'chunks.json', 'metadata.json'];
for (const file of files) {
const srcPath = path.join(this.indexPath, file);
const destPath = path.join(backupDir, file);
if (fsSync.existsSync(srcPath)) {
await fs.copyFile(srcPath, destPath);
}
}
} catch (error) {
console.warn('⚠️ Failed to create backup:', error.message);
}
}
/**
* チャンクIDの生成
*/
generateChunkId(filePath, startLine) {
const hash = this.simpleHash(`${filePath}:${startLine}`);
return `chunk_${hash}`;
}
/**
* 簡単なハッシュ関数
*/
simpleHash(str) {
let hash = 0;
for (let i = 0; i < str.length; i++) {
const char = str.charCodeAt(i);
hash = ((hash << 5) - hash) + char;
hash = hash & hash;
}
return Math.abs(hash).toString(36);
}
/**
* 統計情報の取得
*/
getStats() {
return {
totalChunks: this.indexData.chunks.length,
totalFiles: [...new Set(this.indexData.chunks.map(c => c.metadata.file))].length,
queueLength: this.changeQueue.length,
isWatching: this.watchers.size > 0,
dependencyNodes: this.dependencyGraph.size,
lastUpdate: new Date().toISOString()
};
}
/**
* 監視停止
*/
async stopWatching() {
for (const [path, watcher] of this.watchers.entries()) {
watcher.close();
}
this.watchers.clear();
// デバウンスタイマーの清理
for (const timer of this.debounceTimers.values()) {
clearTimeout(timer);
}
this.debounceTimers.clear();
console.log('🛑 File watching stopped');
}
/**
* クリーンアップ
*/
async cleanup() {
await this.stopWatching();
// 最終的なインデックス保存
if (this.changeQueue.length > 0) {
await this.processChangeQueue();
}
await this.saveIndex();
console.log('🧹 Incremental indexer cleanup completed');
}
}