UNPKG

aiwg

Version:

Deployment tool and support utility for AI context. Copies agents, skills, commands, rules, and behaviors into the paths each AI platform reads (Claude Code, Codex, Copilot, Cursor, Warp, OpenClaw, and 6 more) so one source of truth works across 10 platfo

122 lines 4.68 kB
/** * Drift detection logic. * * Pure functions — given a stored SemanticFields and a current artifact * snapshot, classify status and produce a DriftRow. The CLI layer * orchestrates filesystem I/O around these primitives. * * @implements #1208 */ import { DEFAULT_THRESHOLDS } from './types.js'; const STOP_WORDS = new Set([ 'the', 'a', 'an', 'and', 'or', 'but', 'if', 'on', 'in', 'at', 'to', 'for', 'of', 'is', 'are', 'was', 'were', 'be', 'been', 'this', 'that', 'with', 'from', 'as', 'by', 'it', ]); /** Tokenize and de-stop a string into a content keyword set. */ export function keywords(text) { const tokens = text .toLowerCase() .split(/[^a-z0-9]+/) .filter((t) => t.length > 2 && !STOP_WORDS.has(t)); return new Set(tokens); } /** Jaccard-like overlap ratio between two keyword sets. */ export function keywordOverlap(a, b) { if (a.size === 0 && b.size === 0) return 1; let intersect = 0; for (const t of a) if (b.has(t)) intersect++; const union = a.size + b.size - intersect; return union === 0 ? 1 : intersect / union; } /** Symmetric difference between two symbol arrays. */ export function symbolDelta(stored, current) { const sset = new Set(stored); const cset = new Set(current); const added = []; const removed = []; for (const c of cset) if (!sset.has(c)) added.push(c); for (const s of sset) if (!cset.has(s)) removed.push(s); added.sort(); removed.sort(); return { added, removed }; } /** Classify drift for a single entry. */ export function detectDrift(input) { const t = { ...DEFAULT_THRESHOLDS, ...(input.thresholds ?? {}) }; const now = input.now ?? new Date(); const ageDays = Math.max(0, Math.floor((now.getTime() - new Date(input.stored.enrichedAt).getTime()) / 86_400_000)); // Hash matches → entry is content-fresh; only flag if past freshness window if (input.stored.enrichedHash === input.currentHash) { if (ageDays > t.freshnessMaxDays) { return { artifactId: input.artifactId, status: 'stale-age', reason: `enriched ${ageDays}d ago (>${t.freshnessMaxDays}d threshold), content unchanged`, storedHash: input.stored.enrichedHash, currentHash: input.currentHash, ageDays, }; } return { artifactId: input.artifactId, status: 'ok', reason: 'content unchanged', storedHash: input.stored.enrichedHash, currentHash: input.currentHash, ageDays, }; } // Hash differs but no recomputation supplied → can't classify drift, skip if (!input.recomputed) { return { artifactId: input.artifactId, status: 'skip', reason: 'hash differs but no recomputed enrichment supplied', storedHash: input.stored.enrichedHash, currentHash: input.currentHash, ageDays, }; } // Hash differs AND recomputed → check semantic divergence const overlap = keywordOverlap(keywords(input.stored.summary), keywords(input.recomputed.summary)); const delta = symbolDelta(input.stored.declaredSymbols, input.recomputed.declaredSymbols); const symbolDriftSignificant = t.symbolChangeCritical && (delta.added.length > 0 || delta.removed.length > 0); const overlapDriftSignificant = overlap < t.keywordOverlapMin; if (overlapDriftSignificant || symbolDriftSignificant) { const reasons = []; if (overlapDriftSignificant) reasons.push(`summary overlap ${(overlap * 100).toFixed(0)}% < ${(t.keywordOverlapMin * 100).toFixed(0)}%`); if (symbolDriftSignificant) reasons.push(`symbols changed (+${delta.added.length}/-${delta.removed.length})`); return { artifactId: input.artifactId, status: 'drift', reason: reasons.join('; '), storedHash: input.stored.enrichedHash, currentHash: input.currentHash, ageDays, overlap, symbolDelta: delta, remediation: `aiwg index enrich --using-rlm --files "${input.artifactId}" --force`, }; } return { artifactId: input.artifactId, status: 'ok', reason: 'hash differs but semantic content within thresholds', storedHash: input.stored.enrichedHash, currentHash: input.currentHash, ageDays, overlap, symbolDelta: delta, }; } //# sourceMappingURL=drift.js.map