Files
ruvnet--ruflo/scripts/benchmark-seed-corpus.mjs
wehub-resource-sync 23f7624596
🔄 Automated Rollback Manager / 🔄 Execute Rollback (push) Blocked by required conditions
🔄 Automated Rollback Manager / ✅ Post-Rollback Verification (push) Blocked by required conditions
🔄 Automated Rollback Manager / 📊 Rollback Monitoring (push) Blocked by required conditions
🔄 Automated Rollback Manager / ⏳ Manual Rollback Approval (push) Blocked by required conditions
V3 CI/CD Pipeline / MCP protocol smoke / macos-latest (push) Waiting to run
V3 CI/CD Pipeline / Memory import smoke / macos-latest (push) Waiting to run
V3 CI/CD Pipeline / Windows hook shim smoke (#2132) / macos-latest (push) Waiting to run
V3 CI/CD Pipeline / Windows hook shim smoke (#2132) / windows-latest (push) Waiting to run
V3 CI/CD Pipeline / Windows init hooks smoke (#2132) / macos-latest (push) Waiting to run
V3 CI/CD Pipeline / Windows init hooks smoke (#2132) / windows-latest (push) Waiting to run
V3 CI/CD Pipeline / Windows hook execution smoke (#2132) / macos-latest (push) Waiting to run
V3 CI/CD Pipeline / Windows hook execution smoke (#2132) / windows-latest (push) Waiting to run
V3 CI/CD Pipeline / Witness verify (signed manifest) / macos-latest (push) Blocked by required conditions
V3 CI/CD Pipeline / Witness verify (signed manifest) / ubuntu-latest (push) Blocked by required conditions
V3 CI/CD Pipeline / Witness verify (signed manifest) / windows-latest (push) Blocked by required conditions
V3 CI/CD Pipeline / Publish to npm (alpha) (push) Blocked by required conditions
V3 CI/CD Pipeline / Smoke (no better-sqlite3) / macos-latest / Node 22 (push) Waiting to run
V3 CI/CD Pipeline / Plugin hooks smoke / macos-latest / Node 22 (push) Waiting to run
ADR-166 MCP Bridge Security Lock / Static-source security lock (push) Failing after 0s
ADR-166 MCP Bridge Security Lock / Compose default binds loopback + Mongo has auth (push) Failing after 2s
CodeQL Advanced / Analyze (rust) (push) Failing after 0s
ADR-166 MCP Bridge Security Lock / plugin-agent-federation bindHost default (push) Failing after 1s
ADR-166 MCP Bridge Security Lock / Runtime behavior — 401 + terminal gate + fail-closed (push) Failing after 4s
business-pods-smoke / smoke (push) Failing after 1s
all-plugins-smoke / smoke-all (push) Failing after 2s
CI/CD Pipeline / Security & Code Quality (push) Failing after 1s
CI/CD Pipeline / Test Suite (ubuntu-latest) (push) Failing after 1s
CI/CD Pipeline / Build & Package (macos-latest) (push) Has been skipped
CI/CD Pipeline / Build & Package (ubuntu-latest) (push) Has been skipped
CI/CD Pipeline / Build & Package (windows-latest) (push) Has been skipped
CI/CD Pipeline / Deploy & Release (push) Waiting to run
CI/CD Pipeline / CI Status (push) Waiting to run
CI/CD Pipeline / Documentation & Examples (push) Failing after 1s
Clone Tracker (14-day rolling) / Snapshot clones for ruflo ecosystem (push) Failing after 1s
CodeQL Advanced / Analyze (actions) (push) Failing after 1s
CodeQL Advanced / Analyze (javascript-typescript) (push) Failing after 1s
federation-peer-rust / stable-noop (push) Failing after 1s
metaharness-ci / score (push) Failing after 1s
metaharness-ci / router-compat (push) Failing after 0s
metaharness-ci / similarity-tests (push) Failing after 0s
no-agentbbs-smoke / smoke-without-agentbbs (push) Failing after 1s
V3 CI/CD Pipeline / Build V3 (windows-latest) (push) Has been skipped
codex-integration-audit / Codex integration audit (push) Failing after 1s
helpers-manifest-guard / guard (push) Failing after 1s
🔗 Cross-Agent Integration Tests / 🤝 Agent Coordination Tests (push) Has been skipped
🔗 Cross-Agent Integration Tests / 🧠 Memory Sharing Integration (push) Has been skipped
🔗 Cross-Agent Integration Tests / 🛡️ Fault Tolerance Tests (push) Has been skipped
🔗 Cross-Agent Integration Tests / ⚡ Performance Integration Tests (push) Has been skipped
🔗 Cross-Agent Integration Tests / 📊 Integration Test Report (push) Waiting to run
metaharness-ci / mcp-scan (push) Failing after 1s
metaharness-ci / eject-dryrun (push) Failing after 1s
metaharness-ci / metaharness-real-data (push) Failing after 0s
no-cli-optdep-bloat-2561 / guard (push) Failing after 1s
no-metaharness-smoke / smoke-without-metaharness (push) Failing after 1s
no-phantom-agentic-flow-subpath / guard (push) Failing after 1s
🔄 Automated Rollback Manager / 🚨 Failure Detection (push) Failing after 1s
V3 CI/CD Pipeline / Plugin hooks smoke / ubuntu-latest / Node 22 (push) Failing after 1s
V3 CI/CD Pipeline / ruflo-graph-intelligence build + test smoke (#2044, ADR-123) (push) Failing after 1s
CVE Audit Gate / Audit root (critical-blocking) (push) Failing after 2s
cost-tracker-smoke / smoke (push) Failing after 3s
oia-audit-weekly / audit (push) Failing after 2s
ruflo-agent-smoke / ruflo-agent structural smoke (push) Failing after 1s
📊 Status Badges Update / 📊 Update Status Badges (push) Failing after 1s
V3 CI/CD Pipeline / Static regression guards (#2267 YAML + (push) Failing after 1s
V3 CI/CD Pipeline / Test V3 Packages (push) Failing after 0s
V3 CI/CD Pipeline / agent_execute provider routing smoke (#2042) (push) Failing after 0s
CVE Audit Gate / Audit v3 (critical-blocking) (push) Failing after 1s
federation-peer-rust / stable-native (push) Failing after 2s
🔗 Cross-Agent Integration Tests / 🚀 Integration Test Setup (push) Failing after 2s
neural-trader-smoke / runtime-smoke (push) Failing after 1s
🔄 Automated Rollback Manager / 🔍 Pre-Rollback Validation (push) Waiting to run
V3 CI/CD Pipeline / Build V3 (macos-latest) (push) Has been skipped
V3 CI/CD Pipeline / Build V3 (ubuntu-latest) (push) Has been skipped
V3 CI/CD Pipeline / Type Check V3 (push) Failing after 1s
V3 CI/CD Pipeline / Smoke (no better-sqlite3) / ubuntu-latest / Node 24 (push) Failing after 1s
V3 CI/CD Pipeline / Smoke (no better-sqlite3) / ubuntu-latest / Node 22 (push) Failing after 2s
V3 CI/CD Pipeline / browser rvf create flag smoke (#2015) (push) Failing after 0s
V3 CI/CD Pipeline / Dependency review (#2046) (push) Has been skipped
V3 CI/CD Pipeline / Supply-chain audit (#2046) (push) Failing after 0s
V3 CI/CD Pipeline / witness marker drift smoke (#2021) (push) Failing after 1s
V3 CI/CD Pipeline / neural-trader portfolio CG smoke (#2068, ADR-126 Phase 3) (push) Failing after 1s
V3 CI/CD Pipeline / neural-trader backtest signing smoke (#2068, ADR-126 Phase 4) (push) Failing after 1s
V3 CI/CD Pipeline / kg-extract type-import classification smoke (#2049) (push) Failing after 0s
V3 CI/CD Pipeline / witness verify precondition smoke (#1880) (push) Failing after 2s
V3 CI/CD Pipeline / neural-trader pipeline risk-gate smoke (#2068, ADR-126 Phase 5) (push) Failing after 0s
V3 CI/CD Pipeline / neural-trader feature attribution smoke (#2068, ADR-126 Phase 6) (push) Failing after 0s
V3 CI/CD Pipeline / plugin-registry signature verification smoke (#1922, CWE-347) (push) Failing after 4s
V3 CI/CD Pipeline / memory stats legacy-DB smoke (#2120) (push) Failing after 4s
V3 CI/CD Pipeline / github deprecated actions smoke (#2089, ADR-127 Phase 3) (push) Failing after 1s
V3 CI/CD Pipeline / graph query + pathfinder smoke (ADR-130 P2+P5) (push) Has been skipped
V3 CI/CD Pipeline / graph trajectory hooks smoke (ADR-130 P3) (push) Has been skipped
V3 CI/CD Pipeline / graph plugin adapter smoke (ADR-130 P4) (push) Has been skipped
V3 CI/CD Pipeline / graph benchmark (ADR-130 P6) (push) Has been skipped
V3 CI/CD Pipeline / statusline generator delegation smoke (#2195) (push) Failing after 1s
V3 CI/CD Pipeline / wizard init regression guard (#2206 (push) Failing after 1s
V3 CI/CD Pipeline / memory no-stray-db smoke (ADR-125 P7) (push) Failing after 1s
V3 CI/CD Pipeline / github-safe injection smoke (#2089, ADR-127 Phase 1) (push) Failing after 1s
V3 CI/CD Pipeline / github actions pin smoke (#2089, ADR-127 Phase 1) (push) Failing after 1s
V3 CI/CD Pipeline / github attribution opt-in smoke (#2089, ADR-127 Phase 4) (push) Failing after 1s
V3 CI/CD Pipeline / pre-bash hook safety smoke (#2017) (push) Failing after 1s
V3 CI/CD Pipeline / Memory import smoke / ubuntu-latest (push) Failing after 0s
V3 CI/CD Pipeline / MCP protocol smoke / ubuntu-latest (push) Failing after 2s
V3 CI/CD Pipeline / ruvllm WASM auto-init smoke (#2086) (push) Failing after 4s
V3 CI/CD Pipeline / MCP paired-tool round-trip smoke (#1889) (push) Failing after 1s
V3 CI/CD Pipeline / Plugin package install-safety (#1902/#1903/#1904) (push) Failing after 1s
V3 CI/CD Pipeline / Tool description discoverability (ADR-112) (push) Failing after 3s
V3 CI/CD Pipeline / CLI npx-install smoke (#1147 / (22) (push) Failing after 1s
V3 CI/CD Pipeline / CLI npx-install smoke (#1147 / (24) (push) Failing after 1s
V3 CI/CD Pipeline / Windows hook shim smoke (#2132) / ubuntu-latest (push) Failing after 2s
V3 CI/CD Pipeline / Windows hook execution smoke (#2132) / ubuntu-latest (push) Failing after 1s
V3 CI/CD Pipeline / Windows init hooks smoke (#2132) / ubuntu-latest (push) Failing after 1s
V3 CI/CD Pipeline / Vector-index dimension audit (#1947) (push) Failing after 0s
V3 CI/CD Pipeline / Hook-command install safety (#1921) (push) Failing after 1s
V3 CI/CD Pipeline / ToolOutputGuardrail smoke (ADR-131, (push) Failing after 1s
V3 CI/CD Pipeline / init-bundle invariants smoke (#2095, ADR-128 Phase 5) (push) Failing after 1s
V3 CI/CD Pipeline / wasm provider bridge smoke (ADR-129 P1) (push) Failing after 2s
V3 CI/CD Pipeline / wasm gallery CRUD smoke (ADR-129 P3) (push) Failing after 1s
V3 CI/CD Pipeline / wasm plugin bridge smoke (ADR-129 P4) (push) Failing after 0s
V3 CI/CD Pipeline / wasm compose smoke (ADR-129 P2) (push) Failing after 4s
V3 CI/CD Pipeline / graph schema smoke (ADR-130 P1) (push) Failing after 0s
Validate Marketplace / validate (push) Failing after 1s
🔍 Verification Pipeline / 🚀 Setup Verification (push) Failing after 1s
🔍 Verification Pipeline / 🛡️ Security Verification (push) Has been skipped
🔍 Verification Pipeline / 📝 Code Quality (push) Has been skipped
🔍 Verification Pipeline / 🧪 Test Verification (${{ matrix.os }}, Node ${{ matrix.node }}) (push) Has been skipped
🔍 Verification Pipeline / 🏗️ Build Verification (push) Has been skipped
🔍 Verification Pipeline / 📚 Documentation Verification (push) Has been skipped
🔍 Verification Pipeline / ⚡ Performance Verification (push) Waiting to run
🔍 Verification Pipeline / 📊 Verification Report (push) Waiting to run
CVE Audit Gate / High-severity report (warn only) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:02:19 +08:00

493 lines
23 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env node
/**
* benchmark-seed-corpus.mjs — DRACO-style measurement of the bundled
* seed corpus (ADR-149).
*
* For each row in `v3/@claude-flow/cli/assets/model-router/seed-rows.json`,
* runs the templated task against every candidate model in the registry,
* LLM-judges each response (anthropic/claude-sonnet-4-6 by default) over a
* 3-criterion rubric tuned to the row's expected tier, and writes the
* measured `scores: {model_id: 0..1}` map back to the row.
*
* This is the data lift that makes ADR-149's qualityBar selection actually
* cost-optimal — without measured scores, the bundled KRR fits to assumptions.
*
* USAGE
* # Dry-run (default — no API calls, prints projected cost)
* node scripts/benchmark-seed-corpus.mjs
*
* # Live — REAL OpenRouter API calls, spends real money
* OPENROUTER_API_KEY=sk-or-... node scripts/benchmark-seed-corpus.mjs --live
*
* # Subset (testing): first 8 rows only
* node scripts/benchmark-seed-corpus.mjs --live --max-rows 8
*
* # Custom candidate list
* node scripts/benchmark-seed-corpus.mjs --live --models a,b,c
*
* # Different judge
* node scripts/benchmark-seed-corpus.mjs --live --judge openai/gpt-4.1
*
* COST NOTE: 64 rows × ~5 candidates = ~320 candidate calls + ~320 judge calls.
* Cheap-tier rows are short (~50 input / ~80 output tokens); strong-tier rows
* are longer (~200 input / ~400 output). Projected: ~$1-3 USD. Default
* --max-cost gate is $5.00.
*
* Co-Authored-By: RuFlo <ruv@ruv.net>
*/
import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
import { resolve as resolvePath, dirname } from 'node:path';
import { fileURLToPath } from 'node:url';
const __dirname = dirname(fileURLToPath(import.meta.url));
const REPO_ROOT = resolvePath(__dirname, '..');
const SEED_PATH = resolvePath(REPO_ROOT, 'v3', '@claude-flow', 'cli', 'assets', 'model-router', 'seed-rows.json');
const PROVENANCE_PATH = resolvePath(REPO_ROOT, 'v3', '@claude-flow', 'cli', 'assets', 'model-router', 'seed-rows.provenance.json');
// ============================================================================
// Candidate registry — the models we'll score every row against.
// Prices: USD per million tokens (input / output) as of 2026-06-15.
// ============================================================================
const DEFAULT_CANDIDATES = [
// Cheap-tier (measured 100% pass over 45 runs on the cheap bench)
{ id: 'inclusionai/ling-2.6-flash', in_per_m: 0.01, out_per_m: 0.03, tier: 'haiku' },
{ id: 'google/gemini-2.5-flash-lite', in_per_m: 0.10, out_per_m: 0.40, tier: 'haiku' },
// Cheap-tier control: the Anthropic default
{ id: 'anthropic/claude-haiku-4.5', in_per_m: 1.00, out_per_m: 5.00, tier: 'haiku' },
// Mid-tier (measured 81% / 70% on the midtier bench)
{ id: 'openai/gpt-4.1', in_per_m: 2.00, out_per_m: 8.00, tier: 'sonnet' },
{ id: 'meta-llama/llama-3.3-70b-instruct', in_per_m: 0.13, out_per_m: 0.40, tier: 'sonnet' },
// Mid-tier control: the Anthropic default
{ id: 'anthropic/claude-sonnet-4-6', in_per_m: 3.00, out_per_m: 15.00, tier: 'sonnet' },
// Strong-tier: the Anthropic default (no measured alt yet)
{ id: 'anthropic/claude-opus-4', in_per_m: 15.00, out_per_m: 75.00, tier: 'opus' },
];
const DEFAULT_JUDGE = 'anthropic/claude-sonnet-4-6';
// ============================================================================
// CLI args
// ============================================================================
function parseArgs(argv) {
const a = {
live: false, models: null, judge: DEFAULT_JUDGE, maxCost: 5.00,
maxRows: null, maxTokens: 512, save: true, writeRows: true,
};
for (let i = 2; i < argv.length; i++) {
const k = argv[i];
if (k === '--live') a.live = true;
else if (k === '--models') a.models = argv[++i].split(',').map(s => s.trim()).filter(Boolean);
else if (k === '--judge') a.judge = argv[++i];
else if (k === '--max-cost') a.maxCost = parseFloat(argv[++i]);
else if (k === '--max-rows') a.maxRows = parseInt(argv[++i], 10);
else if (k === '--max-tokens') a.maxTokens = parseInt(argv[++i], 10);
else if (k === '--no-save') a.save = false;
else if (k === '--no-write-rows') a.writeRows = false;
else if (k === '--help' || k === '-h') {
console.log('Usage: node scripts/benchmark-seed-corpus.mjs [--live] [--models a,b,c] [--judge id] [--max-cost USD] [--max-rows N] [--max-tokens N] [--no-save] [--no-write-rows]');
process.exit(0);
}
}
return a;
}
const ARGS = parseArgs(process.argv);
const CANDIDATES = ARGS.models
? DEFAULT_CANDIDATES.filter(m => ARGS.models.includes(m.id)).concat(
ARGS.models.filter(id => !DEFAULT_CANDIDATES.find(m => m.id === id))
.map(id => ({ id, in_per_m: 0, out_per_m: 0, tier: 'unknown' }))
)
: DEFAULT_CANDIDATES;
// ============================================================================
// Load corpus + provenance
// ============================================================================
if (!existsSync(SEED_PATH)) {
console.error(`[bench] seed corpus not found at ${SEED_PATH}`);
process.exit(2);
}
const allRows = JSON.parse(readFileSync(SEED_PATH, 'utf8'));
const provenance = existsSync(PROVENANCE_PATH) ? JSON.parse(readFileSync(PROVENANCE_PATH, 'utf8')) : null;
// ADR-149 v2 — read task+tier directly from each row when present
// (gen-seed-corpus-v2.mjs persists them in the row). Fall back to the v1
// template-regeneration path when the corpus pre-dates v2.
const v2Rows = allRows.every(r => typeof r.task === 'string' && typeof r.tier === 'string');
let enrichedRows;
if (v2Rows) {
enrichedRows = allRows.map((row, idx) => ({
embedding: row.embedding,
scores_prev: row.scores,
task: row.task,
tier: row.tier,
_idx: idx,
}));
console.log(`[bench] v2 corpus detected — using row.task + row.tier directly (${allRows.length} rows).`);
} else {
// v1 fallback — deterministic template regeneration.
const CHEAP_TEMPLATES = [
'rename {x} to {y}',
'add a console.log to {x}',
'fix typo in {x}',
'remove unused import {x}',
'add return type annotation to {x}',
'format {x} as kebab-case',
'increment counter in {x}',
'add try/catch around {x}',
'change var to const in {x}',
'delete unused export {x}',
];
const MID_TEMPLATES = [
'implement a debounce helper for {x}',
'add unit tests for {x}',
'extract a hook from {x}',
'refactor {x} to use async/await',
'add input validation to {x}',
'migrate {x} from callbacks to promises',
'add a logging layer to {x}',
'parameterize {x} with options object',
'add a config schema for {x}',
'write integration tests covering {x}',
];
const STRONG_TEMPLATES = [
'design a distributed consensus protocol with byzantine fault tolerance for {x}',
'audit the {x} authentication flow for OWASP top-10 vulnerabilities',
'architect a multi-tenant database schema with row-level security for {x}',
'analyze why {x} has a memory leak under load — produce hypothesis with evidence',
'refactor {x} to the strategy pattern and migrate all callers safely',
'write a threat model for {x} including STRIDE categorization and mitigations',
'compare CRDT-based and OT-based collaborative editing for {x} with citations',
'design a backwards-compatible API deprecation path for {x}',
'plan a zero-downtime migration of {x} from postgres to a sharded backend',
'reason about consistency guarantees of {x} under partition and recovery',
'debug a nondeterministic race condition in {x} across distributed workers',
'design an event-sourced architecture for {x} with snapshots and replay',
];
const NOUNS = ['cache','session','token','user','order','queue','router','schema','span','tenant','worker','feature-flag','rate-limiter','health-check','rpc-client','migration','dashboard','webhook','indexer','pipeline'];
let _rngSeed = 1234567;
const rng = () => { _rngSeed = (_rngSeed * 16807) % 2147483647; return _rngSeed / 2147483647; };
const reconstructed = [];
for (const [template, tier] of [
...CHEAP_TEMPLATES.map(t => [t, 'cheap']),
...MID_TEMPLATES.map(t => [t, 'mid']),
...STRONG_TEMPLATES.map(t => [t, 'strong']),
]) {
for (let i = 0; i < 2; i++) {
const x = NOUNS[Math.floor(rng() * NOUNS.length)];
const y = NOUNS[Math.floor(rng() * NOUNS.length)];
const task = template.replaceAll('{x}', x).replaceAll('{y}', y);
reconstructed.push({ task, tier, template });
}
}
if (reconstructed.length !== allRows.length) {
console.error(`[bench] row count mismatch: corpus=${allRows.length}, reconstructed=${reconstructed.length}. Regenerate via scripts/gen-seed-corpus-v2.mjs (preferred) or scripts/gen-seed-corpus.mjs.`);
process.exit(3);
}
enrichedRows = allRows.map((row, idx) => ({
embedding: row.embedding,
scores_prev: row.scores,
task: reconstructed[idx].task,
tier: reconstructed[idx].tier,
template: reconstructed[idx].template,
_idx: idx,
}));
console.log(`[bench] v1 corpus detected — regenerating task text from templates (${allRows.length} rows).`);
}
const ROWS = ARGS.maxRows ? enrichedRows.slice(0, ARGS.maxRows) : enrichedRows;
// ============================================================================
// Tier-aware rubrics — different criteria per tier
// ============================================================================
const RUBRICS = {
cheap: [
{ name: 'correct_transform', weight: 0.50, desc: 'The response performs the requested transformation correctly (e.g. var→const, rename X to Y, add console.log, etc.)' },
{ name: 'no_extraneous_prose', weight: 0.25, desc: 'Returns the corrected code/answer without surrounding explanation, markdown, or hedge prose' },
{ name: 'preserves_behavior', weight: 0.25, desc: 'Did not break or omit other parts of the original code/intent' },
],
mid: [
{ name: 'solves_task', weight: 0.40, desc: 'Actually addresses the requested mid-tier work (implements helper, refactors as asked, etc.) with reasonable depth' },
{ name: 'idiomatic', weight: 0.20, desc: 'Code is idiomatic and follows current best practices for the language' },
{ name: 'completeness', weight: 0.20, desc: 'Coverage is complete — does not stub out major parts or punt on the harder half' },
{ name: 'clarity', weight: 0.10, desc: 'Reasoning / structure is clear; would be readable in a real codebase' },
{ name: 'no_extraneous_prose', weight: 0.10, desc: 'Stays close to the requested output format' },
],
strong: [
{ name: 'technical_depth', weight: 0.30, desc: 'Demonstrates real technical depth — not handwaving — on the topic (distributed systems, security, architecture, debugging)' },
{ name: 'tradeoffs', weight: 0.25, desc: 'Surfaces trade-offs, alternatives, and gotchas appropriate to a senior engineer' },
{ name: 'actionable', weight: 0.20, desc: 'The output is actionable — has steps, code, or a concrete plan, not just abstract advice' },
{ name: 'completeness', weight: 0.15, desc: 'Covers the requested scope without major omissions' },
{ name: 'no_extraneous_prose', weight: 0.10, desc: 'Stays close to the requested output format and length' },
],
};
// ============================================================================
// Cost projection
// ============================================================================
// Per-tier avg lengths (approximate; cheap is short, strong is long).
const TIER_TOK = {
cheap: { in: 40, out: 80 },
mid: { in: 100, out: 250 },
strong: { in: 200, out: 450 },
};
const JUDGE_TOK = { in: 600, out: 200 };
function projectedCost() {
let total = 0;
for (const row of ROWS) {
const tok = TIER_TOK[row.tier];
for (const c of CANDIDATES) {
total += (tok.in * c.in_per_m + tok.out * c.out_per_m) / 1_000_000;
}
}
// Judge cost (Sonnet 4.6 listed price: $3 in / $15 out per Mtok)
total += (JUDGE_TOK.in * 3.0 + JUDGE_TOK.out * 15.0) / 1_000_000 * ROWS.length * CANDIDATES.length;
return total;
}
// ============================================================================
// OpenRouter chat-completion call
// ============================================================================
async function callOR(modelId, prompt, apiKey, opts = {}) {
const t0 = performance.now();
const res = await fetch('https://openrouter.ai/api/v1/chat/completions', {
method: 'POST',
headers: {
'Authorization': `Bearer ${apiKey}`,
'Content-Type': 'application/json',
'HTTP-Referer': 'https://github.com/ruvnet/ruflo',
'X-Title': 'ruflo-benchmark-seed-corpus',
},
body: JSON.stringify({
model: modelId,
messages: [{ role: 'user', content: prompt }],
max_tokens: opts.maxTokens ?? ARGS.maxTokens,
temperature: opts.temperature ?? 0.0,
}),
});
const dt = performance.now() - t0;
const text = await res.text();
let body; try { body = JSON.parse(text); } catch { body = { _raw: text }; }
if (!res.ok) return { ok: false, status: res.status, error: body?.error?.message ?? text.slice(0, 200), latencyMs: dt };
return {
ok: true,
content: body?.choices?.[0]?.message?.content ?? '',
latencyMs: dt,
promptTokens: body?.usage?.prompt_tokens ?? 0,
completionTokens: body?.usage?.completion_tokens ?? 0,
};
}
// ============================================================================
// Judge
// ============================================================================
function buildJudgePrompt(row, response) {
const rubric = RUBRICS[row.tier];
const rubricList = rubric.map(r => ` - ${r.name} (weight ${r.weight}): ${r.desc}`).join('\n');
return `You are grading an AI model's response to a coding task. Score each rubric criterion as 0.0 (fails), 0.5 (partial), or 1.0 (meets). Return ONLY a JSON object: {"scores":{"<criterion>":<0|0.5|1>},"comment":"<≤80 char>"}. No prose outside the JSON.
USER PROMPT (tier=${row.tier}):
${row.task}
MODEL'S RESPONSE:
${response}
RUBRIC:
${rubricList}
Be strict — only award 1.0 if the criterion is unambiguously met.`;
}
function aggregateScore(row, judgeJson) {
if (!judgeJson || typeof judgeJson !== 'object') return 0;
const scores = judgeJson.scores ?? {};
const rubric = RUBRICS[row.tier];
let total = 0;
for (const crit of rubric) {
const v = typeof scores[crit.name] === 'number' ? Math.max(0, Math.min(1, scores[crit.name])) : 0;
total += v * crit.weight;
}
return total;
}
async function judge(row, response, apiKey) {
const r = await callOR(ARGS.judge, buildJudgePrompt(row, response), apiKey, { maxTokens: 256, temperature: 0.0 });
if (!r.ok) return { score: 0, usdCost: 0, error: r.error };
const match = r.content.match(/\{[\s\S]*\}/);
let parsed = null;
if (match) { try { parsed = JSON.parse(match[0]); } catch { /* fall through */ } }
const score = aggregateScore(row, parsed);
const usdCost = (r.promptTokens * 3.0 + r.completionTokens * 15.0) / 1_000_000;
return { score, usdCost, comment: parsed?.comment ?? '', raw: parsed };
}
// ============================================================================
// Main
// ============================================================================
async function main() {
const apiKey = process.env.OPENROUTER_API_KEY;
console.log('# Seed-corpus DRACO measurement (ADR-149)\n');
console.log(`- ts: ${new Date().toISOString().slice(0, 19)}Z`);
console.log(`- corpus: ${ROWS.length} rows (${allRows.length} total${ARGS.maxRows ? `, capped to --max-rows=${ARGS.maxRows}` : ''})`);
console.log(`- candidates: ${CANDIDATES.length} (${CANDIDATES.map(c => c.id).join(', ')})`);
console.log(`- judge: ${ARGS.judge}`);
console.log(`- max-tokens per response: ${ARGS.maxTokens}`);
const projected = projectedCost();
console.log(`- projected total cost (incl. judge): ~$${projected.toFixed(4)} USD`);
console.log(`- max-cost gate: $${ARGS.maxCost.toFixed(2)}`);
console.log(`- live mode: ${ARGS.live ? '**YES — real API calls + WILL overwrite seed-rows.json**' : 'no (dry run)'}\n`);
if (!ARGS.live) {
console.log('Dry run — no API calls and no file writes. To run live:');
console.log(` OPENROUTER_API_KEY=sk-or-... node scripts/benchmark-seed-corpus.mjs --live\n`);
console.log('===BENCH_JSON===');
console.log(JSON.stringify({ dryRun: true, projectedCostUSD: projected, candidates: CANDIDATES.map(c => c.id), judge: ARGS.judge, corpusSize: ROWS.length }, null, 2));
return;
}
if (!apiKey) { console.error('[bench] --live requires OPENROUTER_API_KEY in env.'); process.exit(2); }
if (projected > ARGS.maxCost) {
console.error(`[bench] projected $${projected.toFixed(4)} > --max-cost $${ARGS.maxCost.toFixed(2)}; refusing. Override with --max-cost.`);
process.exit(3);
}
// For each row, fan out across candidates, then judge each response.
// Sequential per row (to avoid free-tier rate-limit blasts), parallel
// across candidates within a row.
const measured = [];
let totalModelCost = 0;
let totalJudgeCost = 0;
for (let ri = 0; ri < ROWS.length; ri++) {
const row = ROWS[ri];
const candidateResults = await Promise.all(CANDIDATES.map(async (c) => {
try {
const resp = await callOR(c.id, row.task, apiKey);
if (!resp.ok) return { id: c.id, score: 0, error: resp.error, status: resp.status, usdCost: 0 };
const cCost = (resp.promptTokens * c.in_per_m + resp.completionTokens * c.out_per_m) / 1_000_000;
const j = await judge(row, resp.content, apiKey);
return {
id: c.id, score: j.score, usdCost: cCost, judgeUsdCost: j.usdCost,
latencyMs: resp.latencyMs, comment: j.comment,
promptTokens: resp.promptTokens, completionTokens: resp.completionTokens,
};
} catch (e) {
return { id: c.id, score: 0, error: e instanceof Error ? e.message : String(e), usdCost: 0 };
}
}));
const scores = {};
for (const cr of candidateResults) {
scores[cr.id] = cr.score;
totalModelCost += cr.usdCost ?? 0;
totalJudgeCost += cr.judgeUsdCost ?? 0;
}
measured.push({ idx: row._idx, tier: row.tier, task: row.task, scores, details: candidateResults });
if ((ri + 1) % 5 === 0 || ri === ROWS.length - 1) {
console.log(` [${ri + 1}/${ROWS.length}] tier=${row.tier} \`${row.task.slice(0, 60)}…\` spend so far: model=$${totalModelCost.toFixed(4)} judge=$${totalJudgeCost.toFixed(4)}`);
}
}
console.log(`\nTotal model spend: $${totalModelCost.toFixed(5)}`);
console.log(`Total judge spend: $${totalJudgeCost.toFixed(5)}`);
console.log(`Total spend: $${(totalModelCost + totalJudgeCost).toFixed(5)}`);
// Per-candidate aggregate
const perCandidate = {};
for (const c of CANDIDATES) {
perCandidate[c.id] = { tier: c.tier, scoresByTier: { cheap: [], mid: [], strong: [] }, latencies: [], usdCost: 0, errors: 0 };
}
for (const m of measured) {
for (const cr of m.details) {
const e = perCandidate[cr.id];
if (!e) continue;
if (cr.error) e.errors++;
e.scoresByTier[m.tier].push(cr.score ?? 0);
if (typeof cr.latencyMs === 'number') e.latencies.push(cr.latencyMs);
e.usdCost += cr.usdCost ?? 0;
}
}
const aggRows = CANDIDATES.map(c => {
const e = perCandidate[c.id];
const mean = (arr) => arr.length ? arr.reduce((s, x) => s + x, 0) / arr.length : null;
const meanLat = mean(e.latencies);
return {
id: c.id, tier: c.tier,
cheap_avg_score: mean(e.scoresByTier.cheap),
mid_avg_score: mean(e.scoresByTier.mid),
strong_avg_score: mean(e.scoresByTier.strong),
overall_avg_score: mean([...e.scoresByTier.cheap, ...e.scoresByTier.mid, ...e.scoresByTier.strong]),
latency_mean_ms: meanLat,
usd_cost_total: e.usdCost,
cost_per_m_tok_in: c.in_per_m,
cost_per_m_tok_out: c.out_per_m,
errors: e.errors,
};
});
console.log(`\n| Model | Tier | Cheap | Mid | Strong | Overall | Latency | Errors |`);
console.log(`|---|---|---|---|---|---|---|---|`);
for (const r of aggRows.sort((a, b) => (b.overall_avg_score ?? 0) - (a.overall_avg_score ?? 0))) {
const fmt = (v) => v == null ? '—' : (v * 100).toFixed(1) + '%';
console.log(`| \`${r.id}\` | ${r.tier} | ${fmt(r.cheap_avg_score)} | ${fmt(r.mid_avg_score)} | ${fmt(r.strong_avg_score)} | **${fmt(r.overall_avg_score)}** | ${r.latency_mean_ms ? r.latency_mean_ms.toFixed(0) + ' ms' : '—'} | ${r.errors} |`);
}
console.log();
// Save artifacts
if (ARGS.save) {
const ts = new Date().toISOString().slice(0, 19).replace(/[:T]/g, '-') + 'Z';
const outDir = resolvePath(REPO_ROOT, 'docs', 'benchmarks', 'runs');
mkdirSync(outDir, { recursive: true });
const measuredPath = resolvePath(outDir, `seed-corpus-${ts}.json`);
writeFileSync(measuredPath, JSON.stringify({
meta: { ts: new Date().toISOString(), candidates: CANDIDATES, judge: ARGS.judge, corpusSize: ROWS.length, args: ARGS },
perCandidate: aggRows,
rows: measured,
}, null, 2));
console.log(`Saved measurement: ${measuredPath}`);
}
if (ARGS.writeRows) {
// Overwrite seed-rows.json with measured scores per model id.
// ADR-149 v2 — preserve ALL original row keys (notably `task` + `tier`,
// which gen-seed-corpus-v2.mjs writes per row). The prior stripping
// implementation dropped these fields and broke the v2-detection logic
// in this script's reader on the next run, silently falling back to v1
// template regeneration with a 64-row template count vs the actual 40
// rows in the corpus.
const updated = allRows.map((row, idx) => {
const m = measured.find(x => x.idx === idx);
if (!m) return row;
return { ...row, scores: m.scores };
});
writeFileSync(SEED_PATH, JSON.stringify(updated, null, 0));
console.log(`Wrote measured seed-rows.json (${updated.length} rows)`);
// Update provenance to reflect the measurement run
if (provenance) {
provenance.measured_at = new Date().toISOString();
provenance.measured_against = CANDIDATES.map(c => c.id);
provenance.judge = ARGS.judge;
provenance.measurement_run = `seed-corpus-${new Date().toISOString().slice(0,19).replace(/[:T]/g,'-')}Z.json`;
provenance.caveat = 'Seed corpus is now measured (ADR-149). Re-measure quarterly via `scripts/benchmark-seed-corpus.mjs --live`. To regenerate the embeddings (not the scores), use `scripts/gen-seed-corpus.mjs`.';
writeFileSync(PROVENANCE_PATH, JSON.stringify(provenance, null, 2));
console.log('Updated provenance.');
}
}
console.log('\n===BENCH_JSON===');
console.log(JSON.stringify({ perCandidate: aggRows, totalModelUSD: totalModelCost, totalJudgeUSD: totalJudgeCost }, null, 2));
}
main().catch(e => { console.error('[bench] fatal:', e); process.exit(1); });