UNPKG

arela

Version:

AI-powered CTO with multi-agent orchestration, code summarization, visual testing (web + mobile) for blazing fast development.

161 lines 5.53 kB
import fs from "fs-extra"; import path from "path"; import { computeSemanticHash } from "./semantic-hash.js"; const DEFAULT_TTL_MS = 30 * 24 * 60 * 60 * 1000; // 30 days const DEFAULT_MAX_ENTRIES = 1000; const DEFAULT_PRICE_PER_CALL = 0.0001; // Approximate $ cost per LLM call export class SemanticCache { cacheDir; ttlMs; maxEntries; pricePerCall; log; stats; constructor(projectPath, options = {}) { this.cacheDir = options.cacheDir ?? path.join(projectPath, ".arela", "cache", "summaries"); this.ttlMs = options.ttlMs ?? DEFAULT_TTL_MS; this.maxEntries = options.maxEntries ?? DEFAULT_MAX_ENTRIES; this.pricePerCall = options.pricePerCall ?? DEFAULT_PRICE_PER_CALL; this.log = options.logger ?? ((msg) => console.log(msg)); this.stats = { hits: 0, misses: 0, savings: 0, }; } /** * Get cached summary if semantic contract unchanged. * Returns null on cache miss or expired entry. */ async get(contract) { const hash = computeSemanticHash(contract); const cacheFile = path.join(this.cacheDir, `${hash}.json`); try { if (!(await fs.pathExists(cacheFile))) { this.stats.misses++; return null; } const entry = (await fs.readJSON(cacheFile)); const age = Date.now() - new Date(entry.cachedAt).getTime(); if (age > this.ttlMs) { await fs.remove(cacheFile); this.stats.misses++; return null; } entry.hits += 1; await fs.writeJSON(cacheFile, entry, { spaces: 2 }); this.stats.hits += 1; this.stats.savings += this.pricePerCall; this.log(`✅ Semantic cache HIT: ${contract.filePath} (hash=${hash}, hits=${entry.hits})`); return entry.summary; } catch { // On any cache error, behave as a miss but avoid crashing the caller. this.stats.misses++; return null; } } /** * Store summary in cache (overwriting any existing entry). */ async set(contract, summary) { const hash = computeSemanticHash(contract); const cacheFile = path.join(this.cacheDir, `${hash}.json`); const entry = { semanticHash: hash, summary, contract: { ...contract, }, cachedAt: new Date().toISOString(), hits: 0, }; try { await fs.ensureDir(this.cacheDir); await fs.writeJSON(cacheFile, entry, { spaces: 2 }); await this.enforceSizeLimit(); this.log(`💾 Semantic cache WRITE: ${contract.filePath} (hash=${hash})`); } catch { // Ignore cache write errors; caller's flow should not fail. } } /** * Get cache statistics. */ getStats() { const total = this.stats.hits + this.stats.misses; const hitRate = total === 0 ? 0 : Math.round((this.stats.hits / total) * 100); return { ...this.stats, hitRate, }; } /** * Clear expired cache entries and return the number removed. */ async cleanup() { let removed = 0; try { if (!(await fs.pathExists(this.cacheDir))) { return 0; } const files = await fs.readdir(this.cacheDir); for (const file of files) { const filePath = path.join(this.cacheDir, file); const entry = (await fs.readJSON(filePath)); const age = Date.now() - new Date(entry.cachedAt).getTime(); if (age > this.ttlMs) { await fs.remove(filePath); removed += 1; } } } catch { // Ignore cleanup errors; best-effort only. } return removed; } /** * Remove least-recently-cached entries when over size limit. * Uses cachedAt as a proxy for recency. */ async enforceSizeLimit() { try { if (!(await fs.pathExists(this.cacheDir))) { return; } const files = await fs.readdir(this.cacheDir); if (files.length <= this.maxEntries) { return; } const entries = []; for (const file of files) { const filePath = path.join(this.cacheDir, file); try { const entry = (await fs.readJSON(filePath)); const ts = new Date(entry.cachedAt).getTime() || 0; entries.push({ file: filePath, cachedAt: ts }); } catch { // If a file is unreadable, remove it proactively. await fs.remove(filePath); } } // Sort oldest first and remove until within limit entries.sort((a, b) => a.cachedAt - b.cachedAt); while (entries.length > this.maxEntries) { const oldest = entries.shift(); if (oldest) { await fs.remove(oldest.file); } } } catch { // Ignore size enforcement errors. } } } //# sourceMappingURL=semantic-cache.js.map