phase0-verify.js 7.83 KB
#!/usr/bin/env node
/**
 * scripts/phase0-verify.js
 *
 * DevLoop P0 末端验收检查器(不发起任何子进程,只做静态校验)。
 *
 * Checkpoints:
 *  [1] 35 个 devloop 工具全部 loadable
 *  [2] 6 个 devloop-* agent 在 system/templates/_registry.json 注册
 *  [3] 每个 devloop-* 目录有 prompt.md + config.json (含 cli.toolPolicy)
 *  [4] bash-json-guard 可挂在 Bash preExecute (顶层 index.js 暴露)
 *  [5] services/devloop 7 大件 + tool layer 7 大件 全部 require 成功
 *  [6] mock fixtures 默认+pm 两个文件存在
 *  [7] git-vault + path-resolver 接口存在
 *
 * 通过即 exit 0,任一失败 exit 1。
 *
 * 规格: docs/prd/newfunction/_spec/plan-migration.md · Quick-Start step 7
 */

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

const ROOT = path.resolve(__dirname, '..');
const checks = [];

function check(name, fn) {
  try {
    const r = fn();
    if (r && r.ok === false) checks.push({ name, ok: false, reason: r.reason });
    else checks.push({ name, ok: true, detail: r?.detail || '' });
  } catch (e) {
    checks.push({ name, ok: false, reason: e.message });
  }
}

// ── [1] tools loadable ────────────────────────────────────────
check('35 devloop tools loadable', () => {
  const reg = require('../server/src/tools/devloop/tool-registry');
  reg._resetForTesting();
  const all = reg.loadAllTools();
  const names = reg.listToolNames();
  const expected = [
    // persistence (5)
    'write_todolist', 'update_todo_item', 'update_runtime_state',
    'timeline_append', 'record_memory',
    // doc (12)
    'bootstrap_prd_site', 'bootstrap_sdd_site', 'write_chapter',
    'write_mvcp_page', 'archive_doc', 'diff_html', 'diff_units',
    'echo_diff', 'update_search_index', 'validate_prd_site',
    'validate_sdd_consistency', 'hash_contract',
    // coder (3)
    'contract_bridge', 'sandbox_check', 'append_coder_log',
    // tester (5)
    'parse_vitest', 'hint_builder', 'bug_open', 'bug_transition', 'bug_list',
    // integrator (6)
    'port_picker', 'spawn_units', 'health_wait', 'run_case',
    'parse_placeholder', 'collect_stderr',
    // triager (4)
    'extract_evidence', 'category_classifier',
    'diff_contract_vs_impl', 'find_similar_bug',
  ];
  const missing = expected.filter(n => !names.includes(n));
  if (missing.length) {
    return { ok: false, reason: `missing tools: ${missing.join(', ')}` };
  }
  return { detail: `${all.size} tools registered (expected ≥35)` };
});

// ── [2] _registry.json has 6 devloop-* entries ─────────────────
check('_registry.json has 6 devloop-* agents', () => {
  const regPath = path.join(ROOT, 'system/templates/_registry.json');
  const list = JSON.parse(fs.readFileSync(regPath, 'utf8'));
  const expected = ['devloop-pm', 'devloop-architect', 'devloop-coder',
                    'devloop-tester', 'devloop-integrator', 'devloop-triager'];
  const ids = list.map(x => x.id);
  const missing = expected.filter(e => !ids.includes(e));
  if (missing.length) return { ok: false, reason: `missing: ${missing.join(', ')}` };
  return { detail: `6/6 registered` };
});

// ── [3] per-agent dir has prompt.md + config.json ──────────────
check('per-agent prompt.md + config.json (6 agents)', () => {
  const agents = ['devloop-pm', 'devloop-architect', 'devloop-coder',
                  'devloop-tester', 'devloop-integrator', 'devloop-triager'];
  const issues = [];
  for (const a of agents) {
    const dir = path.join(ROOT, 'system/templates', a);
    const promptOk = fs.existsSync(path.join(dir, 'prompt.md'));
    const cfgPath = path.join(dir, 'config.json');
    const cfgOk = fs.existsSync(cfgPath);
    if (!promptOk) issues.push(`${a}: prompt.md missing`);
    if (!cfgOk) issues.push(`${a}: config.json missing`);
    if (cfgOk) {
      try {
        const cfg = JSON.parse(fs.readFileSync(cfgPath, 'utf8'));
        const tp = cfg.cli?.toolPolicy?.allowedTools;
        if (!Array.isArray(tp) || tp.length === 0) {
          issues.push(`${a}: cli.toolPolicy.allowedTools missing`);
        }
        if (!cfg.sandbox?.writeAllowList) {
          issues.push(`${a}: sandbox.writeAllowList missing`);
        }
      } catch (e) {
        issues.push(`${a}: config.json invalid (${e.message})`);
      }
    }
  }
  if (issues.length) return { ok: false, reason: issues.join('; ') };
  return { detail: '6 agents fully wired' };
});

// ── [4] bash-json-guard exported via index ─────────────────────
check('bash-json-guard wired (devloop/index.js)', () => {
  const idx = require('../server/src/tools/devloop');
  if (!idx.bashHooks || !Array.isArray(idx.bashHooks.preExecute)) {
    return { ok: false, reason: 'bashHooks.preExecute not exported' };
  }
  // 触发一次 inline JSON 应抛
  const hook = idx.bashHooks.preExecute[0];
  let threw = false;
  try { hook(`curl -d '{"k":"v"}' http://x`); } catch { threw = true; }
  if (!threw) return { ok: false, reason: 'guard did not block inline JSON' };
  return { detail: 'jsonGuard blocks inline JSON ✓' };
});

// ── [5] services/devloop modules require successfully ─────────
check('services/devloop 7 modules require', () => {
  const mods = [
    '../server/services/devloop/path-resolver',
    '../server/services/devloop/doc-store',
    '../server/services/devloop/unit-registry',
    '../server/services/devloop/bug-tracker',
    '../server/services/devloop/timeline-logger',
    '../server/services/devloop/changelog-writer',
    '../server/services/devloop/sse-broker',
    '../server/services/devloop/git-vault',
  ];
  for (const m of mods) {
    try { require(m); } catch (e) {
      return { ok: false, reason: `${m}: ${e.message}` };
    }
  }
  return { detail: `${mods.length}/${mods.length} services OK` };
});

// ── [6] mock fixtures present ─────────────────────────────────
check('mock fixtures present', () => {
  const dir = path.join(ROOT, 'fixtures/devloop-mock');
  for (const f of ['default.json', 'devloop-pm.json']) {
    if (!fs.existsSync(path.join(dir, f))) {
      return { ok: false, reason: `missing fixtures/devloop-mock/${f}` };
    }
  }
  return { detail: 'default.json + devloop-pm.json' };
});

// ── [7] git-vault interface present ────────────────────────────
check('git-vault interface', () => {
  const gv = require('../server/services/devloop/git-vault');
  for (const fn of ['init', 'commit', 'status', 'log']) {
    if (typeof gv[fn] !== 'function') {
      return { ok: false, reason: `git-vault.${fn} missing` };
    }
  }
  return { detail: 'init/commit/status/log present' };
});

// ── [8] loop-controller present + git hook callable ───────────
check('loop-controller exposes createProject + commitAfterAgent', () => {
  const lc = require('../server/src/core/devloop/loop-controller');
  for (const fn of ['createProject', 'commitAfterAgent']) {
    if (typeof lc[fn] !== 'function') {
      return { ok: false, reason: `loop-controller.${fn} missing` };
    }
  }
  return { detail: 'createProject + commitAfterAgent present' };
});

// ── 输出 ──────────────────────────────────────────────────────
let pass = 0, fail = 0;
console.log('\n┌─ DevLoop Phase 0 — Static Verification ─────────────');
for (const c of checks) {
  const tag = c.ok ? '✓' : '✗';
  const line = `│ ${tag} ${c.name.padEnd(48)} ${c.ok ? c.detail : '— ' + c.reason}`;
  console.log(line);
  c.ok ? pass++ : fail++;
}
console.log(`└─ ${pass}/${pass + fail} checks passed${fail ? ' · ' + fail + ' failed' : ''}\n`);

process.exit(fail ? 1 : 0);