dist / memory / health.js
dist / memory / health.js
"use strict";
/**
* @file memory/health.ts
* C3: memory health report — pure analytics over memory records, fed into
* the "Memory Status" tool. Read-only; bounded work (dup scan capped).
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.computeHealth = computeHealth;
const tfidf_1 = require("./retrieval/tfidf");
const constants_1 = require("./constants");
const DUP_SCAN_CAP = 500;
const DUP_PAIR_CAP = 20;
const DUP_SIMILARITY = 0.75;
const DAY = 24 * 60 * 60 * 1000;
function jaccard(a, b) {
if (a.size === 0 || b.size === 0)
return 0;
let inter = 0;
for (const t of a)
if (b.has(t))
inter++;
return inter / (a.size + b.size - inter);
}
function computeHealth(memories, now = Date.now()) {
const list = Array.isArray(memories) ? memories : [];
const byCategory = {};
const byScope = {};
const bySource = {};
let confSum = 0;
let lowConfidence = 0;
let stale = 0;
let expiring = 0;
let withExpiry = 0;
for (const m of list) {
if (!m || typeof m.content !== "string")
continue;
byCategory[m.category ?? "note"] = (byCategory[m.category ?? "note"] || 0) + 1;
byScope[m.scope ?? "global"] = (byScope[m.scope ?? "global"] || 0) + 1;
bySource[m.source ?? "user"] = (bySource[m.source ?? "user"] || 0) + 1;
const conf = Number(m.confidence ?? 1);
confSum += conf;
if (conf < 0.5)
lowConfidence++;
if (typeof m.lastAccessedAt === "number" && now - m.lastAccessedAt > constants_1.DECAY_HALF_LIFE_DAYS * DAY)
stale++;
if (typeof m.validTo === "number" && m.validTo) {
withExpiry++;
if (m.validTo - now < 7 * DAY)
expiring++;
}
}
// duplicate candidates: newest-first cap, Jaccard like the dedup rule
const valid = list.filter((m) => m && typeof m.content === "string");
const scanned = valid.slice(0, DUP_SCAN_CAP);
const tokenSets = scanned.map((m) => ({
id: m.id,
set: new Set((0, tfidf_1.tokenize)(String(m.content ?? ""))),
}));
const duplicateCandidates = [];
outer: for (let i = 0; i < tokenSets.length; i++) {
for (let j = i + 1; j < tokenSets.length; j++) {
const sim = jaccard(tokenSets[i].set, tokenSets[j].set);
if (sim >= DUP_SIMILARITY) {
duplicateCandidates.push([tokenSets[i].id, tokenSets[j].id, Math.round(sim * 100) / 100]);
if (duplicateCandidates.length >= DUP_PAIR_CAP)
break outer;
}
}
}
const n = list.length || 1;
return {
total: list.length,
avgConfidence: Math.round((confSum / (valid.length || 1)) * 100) / 100,
lowConfidence,
staleOverHalfLife: stale,
withExpiry,
expiringWithin7d: expiring,
byCategory,
byScope,
bySource,
duplicateCandidates: duplicateCandidates.length ? duplicateCandidates : undefined,
scannedForDuplicates: Math.min(valid.length, DUP_SCAN_CAP),
};
}
//# sourceMappingURL=health.js.map
"use strict";
/**
* @file memory/health.ts
* C3: memory health report — pure analytics over memory records, fed into
* the "Memory Status" tool. Read-only; bounded work (dup scan capped).
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.computeHealth = computeHealth;
const tfidf_1 = require("./retrieval/tfidf");
const constants_1 = require("./constants");
const DUP_SCAN_CAP = 500;
const DUP_PAIR_CAP = 20;
const DUP_SIMILARITY = 0.75;
const DAY = 24 * 60 * 60 * 1000;
function jaccard(a, b) {
if (a.size === 0 || b.size === 0)
return 0;
let inter = 0;
for (const t of a)
if (b.has(t))
inter++;
return inter / (a.size + b.size - inter);
}
function computeHealth(memories, now = Date.now()) {
const list = Array.isArray(memories) ? memories : [];
const byCategory = {};
const byScope = {};
const bySource = {};
let confSum = 0;
let lowConfidence = 0;
let stale = 0;
let expiring = 0;
let withExpiry = 0;
for (const m of list) {
if (!m || typeof m.content !== "string")
continue;
byCategory[m.category ?? "note"] = (byCategory[m.category ?? "note"] || 0) + 1;
byScope[m.scope ?? "global"] = (byScope[m.scope ?? "global"] || 0) + 1;
bySource[m.source ?? "user"] = (bySource[m.source ?? "user"] || 0) + 1;
const conf = Number(m.confidence ?? 1);
confSum += conf;
if (conf < 0.5)
lowConfidence++;
if (typeof m.lastAccessedAt === "number" && now - m.lastAccessedAt > constants_1.DECAY_HALF_LIFE_DAYS * DAY)
stale++;
if (typeof m.validTo === "number" && m.validTo) {
withExpiry++;
if (m.validTo - now < 7 * DAY)
expiring++;
}
}
// duplicate candidates: newest-first cap, Jaccard like the dedup rule
const valid = list.filter((m) => m && typeof m.content === "string");
const scanned = valid.slice(0, DUP_SCAN_CAP);
const tokenSets = scanned.map((m) => ({
id: m.id,
set: new Set((0, tfidf_1.tokenize)(String(m.content ?? ""))),
}));
const duplicateCandidates = [];
outer: for (let i = 0; i < tokenSets.length; i++) {
for (let j = i + 1; j < tokenSets.length; j++) {
const sim = jaccard(tokenSets[i].set, tokenSets[j].set);
if (sim >= DUP_SIMILARITY) {
duplicateCandidates.push([tokenSets[i].id, tokenSets[j].id, Math.round(sim * 100) / 100]);
if (duplicateCandidates.length >= DUP_PAIR_CAP)
break outer;
}
}
}
const n = list.length || 1;
return {
total: list.length,
avgConfidence: Math.round((confSum / (valid.length || 1)) * 100) / 100,
lowConfidence,
staleOverHalfLife: stale,
withExpiry,
expiringWithin7d: expiring,
byCategory,
byScope,
bySource,
duplicateCandidates: duplicateCandidates.length ? duplicateCandidates : undefined,
scannedForDuplicates: Math.min(valid.length, DUP_SCAN_CAP),
};
}
//# sourceMappingURL=health.js.map