changelog-writer.js 2.26 KB
/**
 * DevLoop · changelogWriter
 *
 * 跨 PRD/SDD/units 的版本变更流(M-15)。append-only NDJSON。
 * - 由 DocStore 在 writePRD/writeSDD/writeUnits 成功后调用
 * - 外部 Agent 不直接调
 *
 * 规格:docs/prd/newfunction/_spec/sdd-control.md · C-17
 *      docs/prd/newfunction/_spec/sdd-model.md · M-15
 */

const fs = require('fs');
const path = require('path');

const pathResolver = require('./path-resolver');

function _logPath(pid) { return pathResolver.resolveChangelog(pid); }

const VALID_KINDS = ['prd_rev', 'sdd_rev', 'units_rev', 'integration_spec_rev'];

function append(pid, entry) {
  if (!VALID_KINDS.includes(entry.kind)) {
    throw new Error(`changelogWriter.append: invalid kind "${entry.kind}"`);
  }
  if (typeof entry.rev !== 'number') {
    throw new Error('changelogWriter.append: rev (number) required');
  }
  if (!entry.summary) {
    throw new Error('changelogWriter.append: summary required');
  }

  const full = {
    ts: new Date().toISOString(),
    kind: entry.kind,
    rev: entry.rev,
    by: entry.by || 'system',
    trigger: entry.trigger || 'unknown',
    summary: entry.summary,
    diff: entry.diff || {
      sections_added: [], sections_changed: [], sections_removed: [],
      units_added: [], units_changed: [], units_removed: [],
    },
    archive_ref: entry.archive_ref || null,
  };

  const line = JSON.stringify(full) + '\n';
  const p = _logPath(pid);
  fs.mkdirSync(path.dirname(p), { recursive: true });
  fs.appendFileSync(p, line);
  return full;
}

function read(pid, opts = {}) {
  const p = _logPath(pid);
  if (!fs.existsSync(p)) return [];
  const limit = opts.limit || 100;
  const lines = fs.readFileSync(p, 'utf8').split('\n').filter(Boolean);

  const entries = [];
  for (let i = lines.length - 1; i >= 0; i--) {
    let e;
    try { e = JSON.parse(lines[i]); } catch { continue; }
    if (opts.kind && e.kind !== opts.kind) continue;
    if (opts.since && new Date(e.ts).getTime() < new Date(opts.since).getTime()) break;
    entries.push(e);
    if (entries.length >= limit) break;
  }
  return entries.reverse();
}

/** 取某 kind 最近一条 */
function latest(pid, kind) {
  const recent = read(pid, { kind, limit: 1 });
  return recent[0] || null;
}

module.exports = { append, read, latest, VALID_KINDS };