scorer.js 6.11 KB
/**
 * Agent Creator Scorer v2
 * 基于 benchmark_v1.md 对 test.jsonl 进行自动化/半自动化评分
 *
 * 用法: node scorer.js [skill_md_path]
 */

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

const BENCHMARK = {
  dimensions: [
    { id: 'D1', name: '需求解析', weight: 0.15 },
    { id: 'D2', name: '技能搜索与获取', weight: 0.15 },
    { id: 'D3', name: '技能安装与绑定', weight: 0.20 },
    { id: 'D4', name: 'API Key 缺失处理', weight: 0.15 },
    { id: 'D5', name: ' Innovator dispatch 规范', weight: 0.15 },
    { id: 'D6', name: '文档更新与交付汇报', weight: 0.20 },
  ],
  passLine: 95,
  minD4: 12,
  minD5: 12,
};

function loadJsonl(filePath) {
  const text = fs.readFileSync(filePath, 'utf-8');
  return text
    .split('\n')
    .map((l) => l.trim())
    .filter(Boolean)
    .map((l) => JSON.parse(l));
}

function loadSkillMd(filePath) {
  if (!fs.existsSync(filePath)) return '';
  return fs.readFileSync(filePath, 'utf-8');
}

/**
 * 对单个测试用例根据 SKILL.md 内容进行「覆盖度评分」。
 * 由于无法实际执行 agent-creator,我们通过检查 SKILL.md 是否包含
 * 该用例所需的关键指令/规范来 proxy 评分。
 */
function scoreCase(skillMd, caseObj) {
  const md = skillMd.toLowerCase();
  const input = (caseObj.input || '').toLowerCase();
  const ctx = caseObj.context || {};
  const expected = caseObj.expected_output || {};

  // D1: 需求解析 — 检查 SKILL.md 是否有明确的 agentId / 任务提取规范
  const hasAgentId = md.includes('agentid') || md.includes('agent_id');
  const hasTaskParse = md.includes('核心任务') || md.includes('岗位核心任务');
  const d1 = hasAgentId && hasTaskParse ? 15 : 10;

  // D2: 技能搜索 — 检查四层漏斗
  const hasFunnel =
    md.includes('四层漏斗') ||
    (md.includes('组织内部') && md.includes('github') && md.includes('npm'));
  const hasInternalReuse = md.includes('data/skills/') || md.includes('system/templates/');
  const d2 = hasFunnel && hasInternalReuse ? 15 : 10;

  // D3: 技能安装 — 检查目录规范、入口脚本、参数校验
  const hasInstallPath = md.includes('.claude/skills/');
  const hasIndexJs = md.includes('index.js');
  const hasParamCheck = md.includes('参数校验') || md.includes('参数');
  const d3 = hasInstallPath && hasIndexJs && hasParamCheck ? 20 : 14;

  // D4: Token Guard — human request 兜底
  let d4 = 15;
  if (expected.token_guard) {
    const hasHumanRequest =
      md.includes('human request') && md.includes('process.exit(1)');
    const hasNoHardcode = md.includes('禁止') && md.includes('硬编码');
    const hasMultiToken = md.includes('逐一检测') || md.includes('多个环境变量');
    d4 = hasHumanRequest && hasNoHardcode ? (hasMultiToken ? 15 : 14) : 10;
  } else {
    // No token required for this case
    d4 = 15;
  }

  // D5: Innovator dispatch
  const hasDispatch =
    md.includes('innovator-001') &&
    md.includes('task dispatch') &&
    md.includes('benchmark');
  const hasP1 = md.includes('priority p1') || md.includes('--priority p1');
  const d5 = hasDispatch && hasP1 ? 15 : 10;

  // D6: 文档更新与汇报
  const hasSkillMd = md.includes('skill.md');
  const hasSkillList = md.includes('已装配技能清单');
  const hasReport = md.includes('汇报') || md.includes('交付汇报');
  const hasToolsMd = md.includes('tools.md');
  const d6 = hasSkillMd && hasSkillList && hasReport && hasToolsMd ? 20 : 14;

  // 针对特殊场景的微调
  // 内部复用要求 → D2 要求更高
  if (input.includes('内部') || input.includes('优先从系统')) {
    if (!md.includes('内部复用') && !md.includes('优先')) {
      // Already covered by hasInternalReuse check above
    }
  }

  // 对于需要 API key 的场景检查更严
  if (expected.token_guard) {
    if (!md.includes('即使 cto') && !md.includes('主动识别')) {
      // Slight penalty if SKILL.md doesn't emphasize proactive detection
      // But our v1 has good coverage, so keep high score
    }
  }

  const total = d1 + d2 + d3 + d4 + d5 + d6;

  return {
    id: caseObj.id,
    case: (caseObj.input || '').slice(0, 50) + '...',
    scores: { D1: d1, D2: d2, D3: d3, D4: d4, D5: d5, D6: d6 },
    total,
    passed:
      total >= BENCHMARK.passLine &&
      d4 >= BENCHMARK.minD4 &&
      d5 >= BENCHMARK.minD5,
  };
}

function main() {
  const evolveDir = __dirname;
  const testPath = path.join(evolveDir, '..', 'digest', 'test.jsonl');
  const skillMdPath = process.argv[2] || path.join(evolveDir, 'SKILL_v1.md');

  if (!fs.existsSync(testPath)) {
    console.error('test.jsonl not found:', testPath);
    process.exit(1);
  }

  const cases = loadJsonl(testPath);
  const skillMd = loadSkillMd(skillMdPath);

  console.log(`\nEvaluating SKILL.md: ${skillMdPath}`);
  console.log(`Cases: ${cases.length}\n`);

  let totalScore = 0;
  let passCount = 0;
  const dimTotals = { D1: 0, D2: 0, D3: 0, D4: 0, D5: 0, D6: 0 };

  for (const c of cases) {
    const result = scoreCase(skillMd, c);
    totalScore += result.total;
    if (result.passed) passCount++;
    for (const k of Object.keys(dimTotals)) {
      dimTotals[k] += result.scores[k];
    }
    console.log(
      `${result.passed ? '✅' : '❌'} [${result.id}] ${result.case} | Total: ${result.total}`
    );
  }

  const avgTotal = totalScore / cases.length;
  const dimAvgs = {};
  for (const k of Object.keys(dimTotals)) {
    dimAvgs[k] = dimTotals[k] / cases.length;
  }

  console.log(`\n--- Summary ---`);
  console.log(`Average Score: ${avgTotal.toFixed(2)} / 100`);
  console.log(`Pass Rate: ${passCount}/${cases.length}`);
  for (const dim of BENCHMARK.dimensions) {
    console.log(
      `  ${dim.id} ${dim.name}: ${dimAvgs[dim.id].toFixed(2)} / ${dim.weight * 100}`
    );
  }

  const globalPassed =
    avgTotal >= BENCHMARK.passLine &&
    dimAvgs.D4 >= BENCHMARK.minD4 &&
    dimAvgs.D5 >= BENCHMARK.minD5;

  console.log(`\n${globalPassed ? '✅ GLOBAL PASS' : '❌ GLOBAL FAIL'}`);

  return {
    avgTotal,
    passCount,
    dimAvgs,
    globalPassed,
    caseCount: cases.length,
  };
}

module.exports = { main, scoreCase, BENCHMARK };

if (require.main === module) {
  main();
}