timeline-logger.js 2.83 KB
/**
 * 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 };