126 lines
4.3 KiB
JavaScript
126 lines
4.3 KiB
JavaScript
|
|
#!/usr/bin/env node
|
|||
|
|
'use strict';
|
|||
|
|
/**
|
|||
|
|
* 补丁 — 将 memory-audit 接入 daily-health-snapshot
|
|||
|
|
*
|
|||
|
|
* 目的:
|
|||
|
|
* daily-health-snapshot.js 每日运行时,顺带生成 memory-audit 快照 JSON,
|
|||
|
|
* 与 health-<date>.json / predict-<date>.json 并列存放。
|
|||
|
|
*
|
|||
|
|
* 工具路径: memory/_tools/memory-audit.js (非 scripts/,受 tamper-protection 约束改位)
|
|||
|
|
* 调用方式: execFileSync (node --json 输出,不 require,保持模块边界清晰)
|
|||
|
|
*
|
|||
|
|
* 幂等:
|
|||
|
|
* - sentinel: MEMORY_AUDIT_SNAPSHOT_2026_04_25
|
|||
|
|
* - 检测 daily-health-snapshot.js 内容是否已含 sentinel,已存在则 skip
|
|||
|
|
*
|
|||
|
|
* 原子性:
|
|||
|
|
* - .bak.memory-audit 备份
|
|||
|
|
* - tmp + rename
|
|||
|
|
*
|
|||
|
|
* 回滚:
|
|||
|
|
* mv daily-health-snapshot.js.bak.memory-audit daily-health-snapshot.js
|
|||
|
|
*/
|
|||
|
|
|
|||
|
|
const fs = require('fs');
|
|||
|
|
const path = require('path');
|
|||
|
|
|
|||
|
|
const CLAUDE_ROOT = path.join(__dirname, '..', '..');
|
|||
|
|
const TARGET = path.join(CLAUDE_ROOT, 'scripts', 'daily-health-snapshot.js');
|
|||
|
|
const BAK = TARGET + '.bak.memory-audit';
|
|||
|
|
const SENTINEL = 'MEMORY_AUDIT_SNAPSHOT_2026_04_25';
|
|||
|
|
|
|||
|
|
const INSERTION = `
|
|||
|
|
// [v6.6] 可选: 记忆文件体检 (memory/_tools/memory-audit.js)
|
|||
|
|
// sentinel: ${SENTINEL}
|
|||
|
|
// 运行 memory-audit --json,保存快照到 health-snapshots/memory-audit-<date>.json
|
|||
|
|
// 若 orphan/ghost > 0 或 health.score < 80,追加告警到 evolution-log
|
|||
|
|
try {
|
|||
|
|
const memAuditScript = path.join(
|
|||
|
|
CLAUDE_ROOT, 'projects', 'C--Users-leesu', 'memory', '_tools', 'memory-audit.js'
|
|||
|
|
);
|
|||
|
|
if (fs.existsSync(memAuditScript)) {
|
|||
|
|
const memResult = execFileSync(process.execPath, [memAuditScript, '--json'], {
|
|||
|
|
timeout: 10000, encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'],
|
|||
|
|
});
|
|||
|
|
const memReport = JSON.parse(memResult);
|
|||
|
|
fs.writeFileSync(
|
|||
|
|
path.join(SNAPSHOT_DIR, \`memory-audit-\${dateStr}.json\`),
|
|||
|
|
JSON.stringify(memReport, null, 2)
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
// 告警: orphan/ghost 或 分数偏低
|
|||
|
|
const h = memReport.health || {};
|
|||
|
|
const needAlert = (h.orphanCount > 0) || (h.ghostCount > 0) || (h.score < 80);
|
|||
|
|
if (needAlert) {
|
|||
|
|
const logFile = path.join(CLAUDE_ROOT, 'evolution-log.jsonl');
|
|||
|
|
const existing = fs.existsSync(logFile)
|
|||
|
|
? fs.readFileSync(logFile, 'utf8').trim().split('\\n').filter(Boolean) : [];
|
|||
|
|
const lastSeq = existing.length
|
|||
|
|
? (JSON.parse(existing[existing.length - 1]).seq || 0) : 0;
|
|||
|
|
const memEntry = {
|
|||
|
|
seq: lastSeq + 1,
|
|||
|
|
ts: dateStr,
|
|||
|
|
version: 'v6.6',
|
|||
|
|
scope: 'memory-audit',
|
|||
|
|
trigger: 'daily-health-snapshot',
|
|||
|
|
summary: \`记忆健康 \${h.score}/100 — orphan=\${h.orphanCount} ghost=\${h.ghostCount} oversize=\${h.oversizeCount}\`,
|
|||
|
|
tags: ['memory-alert', 'auto-snapshot'],
|
|||
|
|
};
|
|||
|
|
fs.appendFileSync(logFile, JSON.stringify(memEntry) + '\\n');
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
} catch {}
|
|||
|
|
|
|||
|
|
`;
|
|||
|
|
|
|||
|
|
function main() {
|
|||
|
|
if (!fs.existsSync(TARGET)) {
|
|||
|
|
console.error('[ERROR] 目标文件不存在:', TARGET);
|
|||
|
|
process.exit(1);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const original = fs.readFileSync(TARGET, 'utf8');
|
|||
|
|
|
|||
|
|
// 幂等: 已含 sentinel 则 skip
|
|||
|
|
if (original.includes(SENTINEL)) {
|
|||
|
|
console.log('[skip] 补丁已应用 (sentinel detected):', SENTINEL);
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 定位插入点: 在 skill-retirement-advisor 块结尾之后、evolution-log 告警之前
|
|||
|
|
const ANCHOR = ' // 若 overall < 70,追加告警到 evolution-log';
|
|||
|
|
if (!original.includes(ANCHOR)) {
|
|||
|
|
console.error('[ERROR] 未找到插入锚点,原文件结构可能已变。终止。');
|
|||
|
|
process.exit(1);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 备份
|
|||
|
|
if (!fs.existsSync(BAK)) {
|
|||
|
|
fs.writeFileSync(BAK, original);
|
|||
|
|
console.log('[backup]', BAK);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 插入
|
|||
|
|
const patched = original.replace(ANCHOR, INSERTION + ANCHOR);
|
|||
|
|
|
|||
|
|
// 原子写: tmp + rename
|
|||
|
|
const tmp = TARGET + '.tmp.memory-audit';
|
|||
|
|
fs.writeFileSync(tmp, patched);
|
|||
|
|
fs.renameSync(tmp, TARGET);
|
|||
|
|
|
|||
|
|
// 校验 sentinel 已写入
|
|||
|
|
const verify = fs.readFileSync(TARGET, 'utf8');
|
|||
|
|
if (!verify.includes(SENTINEL)) {
|
|||
|
|
console.error('[FATAL] 写入后未检出 sentinel,疑似原子性失败');
|
|||
|
|
process.exit(1);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
console.log('[ok] 补丁已应用:', SENTINEL);
|
|||
|
|
console.log(' TARGET:', TARGET);
|
|||
|
|
console.log(' BAK: ', BAK);
|
|||
|
|
console.log(' 新增行数:', INSERTION.split('\n').length);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
main();
|