74 lines
4.6 KiB
JavaScript
74 lines
4.6 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* produce-criterion-greens (Level B) — производитель по-критерийного подписанного GREEN.
|
|
* Для каждого листа запечатанного плана, меняющего исходник: прогнать тест критерия (baseline),
|
|
* сломать код мутацией и переспросить тест (P18), посчитать отпечаток (изменённый+тест-файл),
|
|
* produceGreen и дописать в greenRuns-store. Производитель сам гоняет тест+мутацию (SE-LB-7),
|
|
* никаких флагов от контролёра. I/O-main — под живой активацией владельцем; чистые ядра — тестируемы.
|
|
*/
|
|
import { produceGreen, codeFingerprint } from './criterion-green.mjs';
|
|
|
|
const READONLY_OPS = new Set(['Read', 'Grep', 'Glob', 'LS', 'NotebookRead']);
|
|
const WRITE_OPS = new Set(['Write', 'Edit', 'MultiEdit', 'NotebookEdit']);
|
|
|
|
/** Файлы критерия из шага плана. Только файловые мутирующие шаги; иначе null. */
|
|
export function mapStepToFiles(step) {
|
|
if (!step || typeof step !== 'object') return null;
|
|
const op = String(step.op || '');
|
|
if (!WRITE_OPS.has(op) || READONLY_OPS.has(op)) return null;
|
|
const obj = String(step.object || '');
|
|
if (!obj || !obj.endsWith('.mjs')) return null;
|
|
const sourceFile = obj;
|
|
const testFile = obj.endsWith('.test.mjs') ? obj : obj.replace(/\.mjs$/, '.test.mjs');
|
|
return { sourceFile, testFile };
|
|
}
|
|
|
|
/** Чистая обёртка produceGreen: настоящий green ⇔ testPassed && mutationKilled && ключ. */
|
|
export function buildCriterionGreen({ criterion_id, occurrence, code_fingerprint, testPassed, mutationKilled, signerKey, coverage_of_changed = null }) {
|
|
return produceGreen({ criterion_id, occurrence, code_fingerprint, coverage_of_changed, testPassed, mutationKilled, signerKey });
|
|
}
|
|
|
|
// ── I/O-main (под активацией владельцем; не вызывается в TDD-юнитах) ──
|
|
async function main() {
|
|
const { loadFrozenPlan, verifyFrozenPlan, treeLeaves } = await import('./plan-lock.mjs');
|
|
const { resolveReceiptKey } = await import('./receipt-key-config.mjs');
|
|
const { runMutationForFile, classifyMutationResult } = await import('./mutate-runner.mjs');
|
|
const { runVitestJson } = await import('./run-test-json.mjs');
|
|
const { generateMutants } = await import('./mutate-operators.mjs');
|
|
const { readFileSync, existsSync, writeFileSync, mkdirSync } = await import('node:fs');
|
|
const { join } = await import('node:path');
|
|
const { homedir } = await import('node:os');
|
|
|
|
const sess = process.env.CLAUDE_SESSION_ID || 'unknown';
|
|
const gitCwd = process.cwd();
|
|
const key = resolveReceiptKey();
|
|
const runtimeDir = join(homedir(), '.claude', 'runtime');
|
|
const plan = loadFrozenPlan({ sessionId: sess, runtimeDir });
|
|
if (!plan || !verifyFrozenPlan(plan, key)) { process.stdout.write('[produce-criterion-greens] нет валидного запечатанного плана\n'); process.exit(0); }
|
|
|
|
const greens = [];
|
|
let occ = 0;
|
|
for (const leaf of treeLeaves(plan.steps || [])) {
|
|
const files = mapStepToFiles(leaf);
|
|
if (!files) continue;
|
|
const { sourceFile, testFile } = files;
|
|
if (!existsSync(join(gitCwd, sourceFile)) || !existsSync(join(gitCwd, testFile))) {
|
|
greens.push({ criterion_id: leaf.criterion_id, green: false, reason: 'no-test' }); continue;
|
|
}
|
|
const runTest = (tf) => runVitestJson(tf, gitCwd);
|
|
const mut = runMutationForFile({ filePath: join(gitCwd, sourceFile), testFile, generate: generateMutants, runTest });
|
|
const { mutationKilled } = classifyMutationResult({ baselineGreen: mut.baselineGreen, mutantOutcomes: mut.mutantOutcomes });
|
|
const fp = codeFingerprint({ [sourceFile]: readFileSync(join(gitCwd, sourceFile), 'utf-8'), [testFile]: readFileSync(join(gitCwd, testFile), 'utf-8') });
|
|
occ += 1;
|
|
greens.push(buildCriterionGreen({ criterion_id: leaf.criterion_id, occurrence: occ, code_fingerprint: fp, testPassed: mut.baselineGreen, mutationKilled, signerKey: key }));
|
|
}
|
|
try { mkdirSync(runtimeDir, { recursive: true }); } catch { /* ignore */ }
|
|
writeFileSync(join(runtimeDir, `criterion-greens-${sess}.json`), JSON.stringify(greens));
|
|
process.stdout.write(`[produce-criterion-greens] критериев: ${greens.length}, зелёных: ${greens.filter((g) => g.green).length}\n`);
|
|
process.exit(0);
|
|
}
|
|
|
|
import { fileURLToPath } from 'node:url';
|
|
const isCli = process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1];
|
|
if (isCli) main();
|