/** * DevLoop · timelineLogger * * 全流程统一事件日志(M-13 ProcessTimeline)。append-only NDJSON。 * - 每行一个事件 * - 每条 ≤ 1KB(summary 自动截 280 字符;payload > 4KB 强制 ref 形式) * - 同步原子写(appendFileSync) * * 规格:docs/prd/newfunction/_spec/sdd-control.md · C-15 * docs/prd/newfunction/_spec/sdd-model.md · M-13 */ const fs = require('fs'); const path = require('path'); const readline = require('readline'); const pathResolver = require('./path-resolver'); const SUMMARY_MAX = 280; const PAYLOAD_MAX_BYTES = 4096; function _logPath(pid) { return pathResolver.resolveTimelineLog(pid); } function _truncate(s, n) { if (typeof s !== 'string') s = String(s); if (s.length <= n) return s; return s.slice(0, n - 1) + '…'; } /** * Append 一条事件。同步写。 * @param {string} pid * @param {object} ev - { source, kind, ref?, summary, payload? } */ function append(pid, ev) { if (!ev || !ev.source || !ev.kind || !ev.summary) { throw new Error('timelineLogger.append: source/kind/summary required'); } const event = { ts: Math.floor(Date.now() / 1000), source: ev.source, kind: ev.kind, ref: ev.ref || null, summary: _truncate(ev.summary, SUMMARY_MAX), }; // payload 超大 → 强制改 ref 形式(不写盘外部文件,仅写入 ref 占位) if (ev.payload !== undefined) { const json = JSON.stringify(ev.payload); if (json.length > PAYLOAD_MAX_BYTES) { event.payload = { oversize: true, original_bytes: json.length }; } else { event.payload = ev.payload; } } const line = JSON.stringify(event) + '\n'; const p = _logPath(pid); fs.mkdirSync(path.dirname(p), { recursive: true }); fs.appendFileSync(p, line); return event; } /** * 读全部 / 过滤事件。 * @param {object} opts - { limit?, since?: unix ts, source?, kind? } */ function read(pid, opts = {}) { const p = _logPath(pid); if (!fs.existsSync(p)) return []; const limit = opts.limit || 100; const since = opts.since; const filterSource = opts.source; const filterKind = opts.kind; const lines = fs.readFileSync(p, 'utf8').split('\n').filter(Boolean); const events = []; // 倒序读,遇到 limit 停(避免 load 整个 file) for (let i = lines.length - 1; i >= 0; i--) { let ev; try { ev = JSON.parse(lines[i]); } catch { continue; } if (since && ev.ts < since) break; // 由于 ts 单调,可以提前 break if (filterSource && ev.source !== filterSource) continue; if (filterKind && ev.kind !== filterKind) continue; events.push(ev); if (events.length >= limit) break; } return events.reverse(); // 输出按时间正序 } /** 最近 N 条(已按时间正序) */ function tail(pid, n = 10) { return read(pid, { limit: n }); } module.exports = { append, read, tail };