Files
portal/tools/produce-verify-receipt.mjs
T

83 lines
4.5 KiB
JavaScript
Raw 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
/**
* produce-verify-receipt (G1) — производитель подписанной verify-расписки.
* Чистое ядро buildVerifyReceipt: настоящий green ⇔ suitePassed===true И есть ключ.
* I/O-обёртка main сама прогоняет сюиту (флаг не от контроллера, SE-G1-5), считает
* fingerprint staged-diff, инкрементит occurrence, персистит в protected runtime.
*
* Файл расписки — ЕДИНЫЙ `~/.claude/runtime/verify-receipt.json` (НЕ session-scoped):
* producer (CLI) не получает session_id из stdin как consumer-хук, поэтому единый путь
* исключает рассинхрон. Безопасно для Level A — свежесть держит fingerprint (старая
* расписка ≠ текущему staged-diff → stale-fingerprint → block у consumer).
*/
import { signVerifyReceipt } from './verify-receipt.mjs';
import { codeFingerprint } from './criterion-green.mjs';
import { execFileSync, execSync } from 'node:child_process';
import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'node:fs';
import { join } from 'node:path';
import { homedir } from 'node:os';
import { fileURLToPath } from 'node:url';
export function buildVerifyReceipt({ codeFingerprint: fp, lastOccurrence = 0, suitePassed = false, signerKey } = {}) {
if (suitePassed !== true) return { ok: false, reason: 'suite-not-passed' };
const occurrence = lastOccurrence + 1;
const receipt = signVerifyReceipt({ code_fingerprint: fp, occurrence }, signerKey);
if (!receipt) return { ok: false, reason: 'no-signer-key' };
return { ok: true, receipt };
}
/** Отпечаток staged-diff: sha256 канонизированной карты {путь: содержимое} (Δ2 через codeFingerprint). */
export function stagedFingerprint(fileContents = {}) {
return codeFingerprint(fileContents);
}
const RECEIPT_FILE = 'verify-receipt.json';
function gitTopLevel() {
return execFileSync('git', ['rev-parse', '--show-toplevel'], { encoding: 'utf-8' }).trim();
}
function stagedFiles(gitCwd) {
return execFileSync('git', ['-C', gitCwd, 'diff', '--staged', '--name-only'], { encoding: 'utf-8' })
.split(/\r?\n/).map((s) => s.trim()).filter(Boolean);
}
function stagedContents(gitCwd, files) {
const map = {};
for (const f of files) { try { map[f] = readFileSync(join(gitCwd, f), 'utf-8'); } catch { /* deleted/binary — skip */ } }
return map;
}
/** I/O-main: staged → fingerprint → прогон сюиты → buildVerifyReceipt → персист в protected runtime. */
async function main() {
const dir = join(homedir(), '.claude', 'runtime');
const path = join(dir, RECEIPT_FILE);
const gitCwd = gitTopLevel();
const fp = stagedFingerprint(stagedContents(gitCwd, stagedFiles(gitCwd)));
// Producer САМ прогоняет сюиту (SE-G1-5: флаг не от контроллера). Exit 0 = passed.
let suitePassed = false;
try {
// Windows-safe (SE-G1-5 сохранён: флаг не от контроллера): npx — это npx.cmd, и
// execFileSync('npx') без shell его НЕ находит → бросает → suite-not-passed навсегда.
// execSync со строкой + кавычки на путях (в пути репо есть пробелы) запускает реально.
const appRoot = join(gitCwd, 'app');
const cfg = join(gitCwd, 'app', 'vitest.config.tools.mjs');
execSync(`npx vitest run --root "${appRoot}" --config "${cfg}" --reporter dot`,
{ cwd: gitCwd, stdio: 'ignore' });
suitePassed = true;
} catch { suitePassed = false; }
const { resolveReceiptKey } = await import('./receipt-key-config.mjs');
let last = 0;
if (existsSync(path)) { try { last = JSON.parse(readFileSync(path, 'utf-8')).occurrence || 0; } catch { last = 0; } }
const r = buildVerifyReceipt({ codeFingerprint: fp, lastOccurrence: last, suitePassed, signerKey: resolveReceiptKey() });
try { mkdirSync(dir, { recursive: true }); } catch {}
if (r.ok) {
writeFileSync(path, JSON.stringify(r.receipt));
process.stdout.write(`[produce-verify-receipt] signed GREEN: fp=${fp.slice(0, 12)} occ=${r.receipt.occurrence}\n`);
} else {
process.stdout.write(`[produce-verify-receipt] NOT signed: ${r.reason}\n`);
}
process.exit(0);
}
const isCli = process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1];
if (isCli) main();