bookworm-smart-assistant/scripts/patches/patch-memory-audit-snapshot.js
Bookworm Admin b7a8e29d21 release: v6.7.0 - OTA E2E test release
- VERSION file as authoritative version source
- export.mjs reads VERSION with package.json fallback
- bw-ota.ps1 DryRun mode for safe testing
- auto-setup.ps1 bumped to v3.2.0 (Phase 8 OTA)
2026-04-27 17:59:44 +08:00

126 lines
4.3 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/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();