mirror of
https://github.com/pbakaus/impeccable.git
synced 2026-09-12 22:26:38 +03:00
Addresses the review findings on #371, plus several the bots did not catch. All fixes have regression coverage that fails on the prior code. Source corruption: - Vue accept dropped valueless root attrs (disabled, v-cloak) and, worse, rewrote @click="x" as a literal click="x" DOM attribute, because the attr parser was name-anchored and skipped the sigil. Tokenize the whole Vue attr grammar and normalize shorthands so accept round-trips directives. - --variant was interpolated unescaped into a RegExp, so --variant '.*' matched the original block first and reported a successful accept while silently restoring the original. Validate against the digits pattern the browser and the /events schema already enforce. - --id reached path.join unvalidated, so --id ../../../../etc/evil wrote and read receipts outside the project. Hoist the existing safeSessionId check into impeccable-paths and apply it at every id-to-path sink. Accept/lock correctness: - Plain HTML/JSX accept and discard did not catch SOURCE_LOCKED, so contention exited non-zero with empty stdout and the agent got no JSON to retry on. - Lock staleness was mtime-only and never read the pid it records: a holder whose critical section outran 60s had its live lock swept, admitting a second writer to the same file, while a crashed holder blocked accepts for a full 60s. Decide staleness by owner liveness, and release only our own lock. Detector: - isNeutralColor only parses computed color forms, so routing authored CSS through it reported inset 4px 0 0 #000 / black / #e5e7eb as chromatic side-tab stripes. Add an authored-color neutrality test covering hex and named neutrals; the fixture had no literal-color cases at all. - Rule line numbers were off by one for every rule after the first, and commented-out CSS was scanned as live rules. Server: - An error reply carries no sourceEventType, and inferSourceEventType returned undefined, which acknowledgePendingEvent treats as a wildcard: a stale generate worker's failure consumed the user's queued Accept, which then reached no agent and left the browser in SAVING forever. - The generate preflight spawned live-wrap.mjs synchronously inside the request handler, freezing the single-threaded server for the whole scaffold (~7.6s measured on this repo, 15s ceiling) and stalling Accept/Discard/SSE. Make it async, claiming the lease before the first await so no event double-delivers. - Every browser checkpoint was echoed back as variant_progress, so a Tune slider drag remounted the preview under the user's cursor and latched the *_reviewable phases from the wrong trigger. Gate on the reason. Cleanup: - Collapse four divergent benchmark argv parsers into scripts/lib/cli-args.mjs. Three silently misread flags: --iterations 20 benchmarked 5, --agent llm ran the fake agent, --median-target=0.4 used the default threshold. - Drop a snapshot cache this branch made write-only (it grew per session for the server's lifetime and was never read), a dead exported reconcile helper, and the unused deferReply branch. Prepared with AI assistance under maintainer direction. Co-Authored-By: Claude <noreply@anthropic.com>
510 lines
20 KiB
JavaScript
510 lines
20 KiB
JavaScript
#!/usr/bin/env node
|
|
|
|
import { execFile } from 'node:child_process';
|
|
import { readFile, writeFile, mkdir } from 'node:fs/promises';
|
|
import { dirname, join, resolve } from 'node:path';
|
|
import { performance } from 'node:perf_hooks';
|
|
import { promisify } from 'node:util';
|
|
import { fileURLToPath } from 'node:url';
|
|
|
|
import { bootFixtureSession, FIXTURES_DIR } from '../tests/live-e2e/session.mjs';
|
|
import { createFakeAgent } from '../tests/live-e2e/agent.mjs';
|
|
import { parseArgs, positiveIntFlag } from './lib/cli-args.mjs';
|
|
import {
|
|
clickAccept,
|
|
clickGo,
|
|
pickElement,
|
|
waitForCycling,
|
|
waitForHandshake,
|
|
} from '../tests/live-e2e/ui.mjs';
|
|
import {
|
|
BRAND_CONTRACT,
|
|
PROVIDER_PROFILES,
|
|
STRATEGIES,
|
|
applyRuntimeSourceScore,
|
|
createProviderLiveAgent,
|
|
loadBenchmarkEnv,
|
|
resolveProviderSelection,
|
|
scoreVariantOutput,
|
|
summarizeProviderRuns,
|
|
validateAcceptedCleanup,
|
|
} from './lib/live-provider-benchmark.mjs';
|
|
|
|
const execFileP = promisify(execFile);
|
|
const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..');
|
|
const FIXTURE_NAME = 'vite8-react-brand-fidelity';
|
|
const SOURCE_FILE = 'src/App.jsx';
|
|
const args = parseArgs(process.argv.slice(2));
|
|
const iterations = positiveIntFlag(args.iterations, 1);
|
|
const providers = csv(args.providers || 'anthropic,openai,google');
|
|
const strategies = csv(args.strategies || Object.keys(STRATEGIES).join(','));
|
|
const outputPath = args.output ? resolve(ROOT, String(args.output)) : null;
|
|
const loadedEnv = loadBenchmarkEnv({ repoRoot: ROOT, explicitPath: args.envFile ? resolve(String(args.envFile)) : null });
|
|
const modelOverrides = Object.fromEntries(providers.map((provider) => [provider, args[`${provider}Model`]]).filter(([, value]) => value));
|
|
const selection = resolveProviderSelection(providers, modelOverrides);
|
|
const fixture = JSON.parse(await readFile(join(FIXTURES_DIR, FIXTURE_NAME, 'fixture.json'), 'utf-8'));
|
|
const liveSpec = await readFile(join(ROOT, 'skill', 'reference', 'live.md'), 'utf-8');
|
|
|
|
validateConfiguration({ fixture, strategies, selection, liveSpec });
|
|
|
|
if (args.dryRun) {
|
|
const report = {
|
|
schemaVersion: 1,
|
|
generatedAt: new Date().toISOString(),
|
|
mode: 'dry-run',
|
|
fixture: FIXTURE_NAME,
|
|
iterations,
|
|
envFilesLoaded: loadedEnv.length,
|
|
providers: selection.map(publicProviderSelection),
|
|
strategies: strategies.map((strategy) => ({ strategy, ...STRATEGIES[strategy] })),
|
|
plannedApiCallsPerIteration: Object.fromEntries(strategies.map((strategy) => [strategy, callsPerStrategy(strategy)])),
|
|
qualityGate: qualityGateDescription(),
|
|
};
|
|
if (outputPath) await persist(report, outputPath);
|
|
process.stdout.write(JSON.stringify(report, null, 2) + '\n');
|
|
process.exit(0);
|
|
}
|
|
|
|
const available = selection.filter((item) => item.keyPresent);
|
|
if (args.requireAll && available.length !== selection.length) {
|
|
const missing = selection.filter((item) => !item.keyPresent).map((item) => item.provider);
|
|
throw new Error(`missing API keys for: ${missing.join(', ')}`);
|
|
}
|
|
if (available.length === 0) throw new Error('no provider API keys found; use --dry-run to validate without network calls');
|
|
|
|
const needsBrowser = args.pipeline === 'e2e' || args.skipCleanupControl !== true;
|
|
const { chromium } = needsBrowser ? await import('playwright') : { chromium: null };
|
|
const browser = chromium ? await chromium.launch({ headless: args.headed !== true }) : null;
|
|
const results = [];
|
|
let cleanupControl = { passed: true, skipped: true };
|
|
try {
|
|
if (args.skipCleanupControl !== true) {
|
|
process.stderr.write('[live-provider-bench] running provider-independent Accept/cleanup control\n');
|
|
cleanupControl = await runCleanupControl({ browser, fixture });
|
|
}
|
|
if (args.cleanupOnly) {
|
|
const cleanupReport = {
|
|
schemaVersion: 1,
|
|
generatedAt: new Date().toISOString(),
|
|
mode: 'cleanup-control',
|
|
fixture: FIXTURE_NAME,
|
|
cleanupControl,
|
|
};
|
|
if (outputPath) await persist(cleanupReport, outputPath);
|
|
process.stdout.write(JSON.stringify(cleanupReport, null, 2) + '\n');
|
|
process.exitCode = cleanupControl.passed ? 0 : 1;
|
|
}
|
|
if (args.cleanupOnly) {
|
|
// Skip provider calls; the finally block still closes Chromium.
|
|
} else {
|
|
for (const providerConfig of available) {
|
|
for (const strategy of strategies) {
|
|
for (let iteration = 1; iteration <= iterations; iteration += 1) {
|
|
process.stderr.write(`[live-provider-bench] ${providerConfig.provider}/${providerConfig.model} ${strategy} run ${iteration}/${iterations}\n`);
|
|
results.push(args.pipeline === 'e2e'
|
|
? await runOne({ browser, fixture, liveSpec, providerConfig, strategy, iteration })
|
|
: await runGenerationOne({ liveSpec, providerConfig, strategy, iteration, cleanupControl }));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
} finally {
|
|
if (browser) await browser.close().catch(() => {});
|
|
}
|
|
|
|
if (args.cleanupOnly) process.exit(process.exitCode || 0);
|
|
|
|
const groups = [];
|
|
for (const providerConfig of selection) {
|
|
for (const strategy of strategies) {
|
|
const runs = results.filter((run) => run.provider === providerConfig.provider && run.strategy === strategy);
|
|
if (runs.length === 0) continue;
|
|
groups.push({
|
|
provider: providerConfig.provider,
|
|
model: providerConfig.model,
|
|
strategy,
|
|
strategyConfig: STRATEGIES[strategy],
|
|
summary: summarizeProviderRuns(runs),
|
|
runs,
|
|
});
|
|
}
|
|
}
|
|
|
|
const report = {
|
|
schemaVersion: 1,
|
|
generatedAt: new Date().toISOString(),
|
|
mode: 'live',
|
|
fixture: FIXTURE_NAME,
|
|
iterations,
|
|
providers: selection.map(publicProviderSelection),
|
|
qualityGate: qualityGateDescription(),
|
|
cleanupControl,
|
|
groups,
|
|
evaluations: evaluateStrategies(groups),
|
|
totals: {
|
|
apiCalls: results.reduce((sum, run) => sum + run.providerCalls.filter((call) => call.phase !== 'parallel-assembled').length, 0),
|
|
estimatedCostUsd: roundUsd(results.reduce((sum, run) => sum + run.estimatedCostUsd, 0)),
|
|
passingRuns: results.filter((run) => run.passed).length,
|
|
totalRuns: results.length,
|
|
},
|
|
};
|
|
|
|
if (outputPath) await persist(report, outputPath);
|
|
process.stdout.write(JSON.stringify(report, null, 2) + '\n');
|
|
|
|
async function runGenerationOne({ liveSpec: loadedLiveSpec, providerConfig, strategy, iteration, cleanupControl: cleanup }) {
|
|
const records = [];
|
|
const agent = createProviderLiveAgent({
|
|
provider: providerConfig.provider,
|
|
model: providerConfig.model,
|
|
strategy,
|
|
liveSpec: loadedLiveSpec,
|
|
onRecord: (record) => {
|
|
records.push(record);
|
|
const result = record.error ? `error=${record.error.split('\n')[0]}` : `duration=${record.durationMs ?? 0}ms`;
|
|
process.stderr.write(`[live-provider-bench:model] ${record.phase}${record.lane ? `/${record.lane}` : ''} attempt=${record.attempt} ${result}\n`);
|
|
},
|
|
});
|
|
const event = syntheticEvent(`${providerConfig.provider}-${strategy}-${iteration}`);
|
|
const startedAt = performance.now();
|
|
try {
|
|
let output;
|
|
let firstOutput;
|
|
let firstReviewableMs;
|
|
if (typeof agent.generateFirstVariant === 'function') {
|
|
firstOutput = await agent.generateFirstVariant(event, {});
|
|
firstReviewableMs = roundMs(performance.now() - startedAt);
|
|
output = await agent.generateRemainingVariants(event, { firstOutput });
|
|
} else {
|
|
output = await agent.generateVariants(event, {});
|
|
firstReviewableMs = roundMs(performance.now() - startedAt);
|
|
}
|
|
const allReadyMs = roundMs(performance.now() - startedAt);
|
|
const quality = scoreVariantOutput(output);
|
|
const estimatedCostUsd = roundUsd(records.reduce((sum, record) => sum + Number(record.estimatedCostUsd || 0), 0));
|
|
return {
|
|
provider: providerConfig.provider,
|
|
model: providerConfig.model,
|
|
strategy,
|
|
iteration,
|
|
firstReviewableMs,
|
|
allReadyMs,
|
|
acceptCleanupMs: cleanup.acceptCleanupMs ?? null,
|
|
quality,
|
|
cleanup,
|
|
firstOutputScore: firstOutput ? scoreVariantOutput(firstOutput) : quality,
|
|
providerCalls: records.map(publicProviderRecord),
|
|
estimatedCostUsd,
|
|
passed: quality.passed && cleanup.passed,
|
|
};
|
|
} catch (error) {
|
|
return {
|
|
provider: providerConfig.provider,
|
|
model: providerConfig.model,
|
|
strategy,
|
|
iteration,
|
|
error: String(error?.stack || error),
|
|
cleanup,
|
|
providerCalls: records.map(publicProviderRecord),
|
|
estimatedCostUsd: roundUsd(records.reduce((sum, record) => sum + Number(record.estimatedCostUsd || 0), 0)),
|
|
passed: false,
|
|
};
|
|
}
|
|
}
|
|
|
|
async function runCleanupControl({ browser, fixture: loadedFixture }) {
|
|
let session;
|
|
try {
|
|
session = await bootFixtureSession({
|
|
name: FIXTURE_NAME,
|
|
fixture: loadedFixture,
|
|
browser,
|
|
agent: createFakeAgent(),
|
|
wrapTarget: { classes: 'offer-card', tag: 'article', text: 'Field Notes' },
|
|
progressive: false,
|
|
log: args.verbose ? (message) => process.stderr.write(`[live-provider-bench:cleanup] ${message}\n`) : () => {},
|
|
});
|
|
await waitForHandshake(session.page);
|
|
await pickElement(session.page, loadedFixture.runtime.pickSelector);
|
|
await clickGo(session.page);
|
|
await waitForCycling(session.page, 3, { timeout: 45_000 });
|
|
const acceptAt = performance.now();
|
|
await clickAccept(session.page, { expectedVariant: 1 });
|
|
const browserClean = await waitForAcceptCleanup(session.page, session.tmp);
|
|
const acceptCleanupMs = roundMs(performance.now() - acceptAt);
|
|
const source = await readFile(join(session.tmp, SOURCE_FILE), 'utf-8');
|
|
const build = args.skipBuild ? { passed: true, skipped: true } : await verifyBuild(session.tmp);
|
|
return {
|
|
...validateAcceptedCleanup({ source, browserClean, buildPassed: build.passed }),
|
|
acceptCleanupMs,
|
|
build,
|
|
consoleErrorCount: session.consoleErrors.length,
|
|
};
|
|
} catch (error) {
|
|
return { passed: false, error: String(error?.stack || error) };
|
|
} finally {
|
|
if (session) await session.teardown();
|
|
}
|
|
}
|
|
|
|
async function runOne({ browser, fixture, liveSpec, providerConfig, strategy, iteration }) {
|
|
const records = [];
|
|
const agent = createProviderLiveAgent({
|
|
provider: providerConfig.provider,
|
|
model: providerConfig.model,
|
|
strategy,
|
|
liveSpec,
|
|
onRecord: (record) => {
|
|
records.push(record);
|
|
const result = record.error ? `error=${record.error.split('\n')[0]}` : `duration=${record.durationMs ?? 0}ms`;
|
|
process.stderr.write(`[live-provider-bench:model] ${record.phase}${record.lane ? `/${record.lane}` : ''} attempt=${record.attempt} ${result}\n`);
|
|
},
|
|
});
|
|
let session;
|
|
const startedAt = performance.now();
|
|
try {
|
|
session = await bootFixtureSession({
|
|
name: FIXTURE_NAME,
|
|
fixture,
|
|
browser,
|
|
agent,
|
|
wrapTarget: (event) => ({
|
|
classes: event.element?.classes?.join(',') || 'offer-card',
|
|
tag: event.element?.tagName?.toLowerCase() || 'article',
|
|
text: event.element?.textContent?.trim(),
|
|
}),
|
|
progressive: STRATEGIES[strategy].delivery !== 'atomic',
|
|
log: args.verbose ? (message) => process.stderr.write(`[live-provider-bench:e2e] ${message}\n`) : () => {},
|
|
});
|
|
await waitForHandshake(session.page);
|
|
await pickElement(session.page, fixture.runtime.pickSelector);
|
|
|
|
const goAt = performance.now();
|
|
const firstReady = waitForFirstReviewable(session.page);
|
|
await clickGo(session.page);
|
|
await firstReady;
|
|
const firstReviewableMs = roundMs(performance.now() - goAt);
|
|
await waitForCycling(session.page, 3, { timeout: 240_000 });
|
|
const allReadyMs = roundMs(performance.now() - goAt);
|
|
|
|
const finalRecord = [...records].reverse().find((record) => ['atomic', 'remaining', 'parallel-assembled'].includes(record.phase) && record.output);
|
|
if (!finalRecord) throw new Error('provider benchmark produced no complete variant output');
|
|
let quality = scoreVariantOutput(finalRecord.output);
|
|
|
|
const acceptAt = performance.now();
|
|
await clickAccept(session.page, { expectedVariant: 1 });
|
|
const browserClean = await waitForAcceptCleanup(session.page, session.tmp);
|
|
const acceptCleanupMs = roundMs(performance.now() - acceptAt);
|
|
const source = await readFile(join(session.tmp, SOURCE_FILE), 'utf-8');
|
|
const build = args.skipBuild ? { passed: true, skipped: true } : await verifyBuild(session.tmp);
|
|
const cleanup = validateAcceptedCleanup({ source, browserClean, buildPassed: build.passed });
|
|
quality = applyRuntimeSourceScore(quality, cleanup);
|
|
|
|
const estimatedCostUsd = roundUsd(records.reduce((sum, record) => sum + Number(record.estimatedCostUsd || 0), 0));
|
|
const passed = quality.passed && cleanup.passed;
|
|
return {
|
|
provider: providerConfig.provider,
|
|
model: providerConfig.model,
|
|
strategy,
|
|
iteration,
|
|
firstReviewableMs,
|
|
allReadyMs,
|
|
acceptCleanupMs,
|
|
endToEndMs: roundMs(performance.now() - startedAt),
|
|
quality,
|
|
cleanup,
|
|
build,
|
|
consoleErrorCount: session.consoleErrors.length,
|
|
providerCalls: records.map(publicProviderRecord),
|
|
estimatedCostUsd,
|
|
passed,
|
|
};
|
|
} catch (error) {
|
|
return {
|
|
provider: providerConfig.provider,
|
|
model: providerConfig.model,
|
|
strategy,
|
|
iteration,
|
|
error: String(error?.stack || error),
|
|
providerCalls: records.map(publicProviderRecord),
|
|
estimatedCostUsd: roundUsd(records.reduce((sum, record) => sum + Number(record.estimatedCostUsd || 0), 0)),
|
|
passed: false,
|
|
};
|
|
} finally {
|
|
if (session) await session.teardown();
|
|
}
|
|
}
|
|
|
|
async function waitForFirstReviewable(page) {
|
|
await page.waitForFunction(() => {
|
|
const query = window.__impeccableLiveQuery || ((selector) => document.querySelector(selector));
|
|
const wrapper = query('[data-impeccable-variants]');
|
|
if (!wrapper) return false;
|
|
const sourceVariants = wrapper.querySelectorAll('[data-impeccable-variant]:not([data-impeccable-variant="original"])');
|
|
const debug = window.__IMPECCABLE_LIVE_CHROME_CORE__?.debugState?.();
|
|
const arrived = wrapper.dataset.impeccablePreview === 'svelte-component'
|
|
? Number(debug?.arrivedVariants || 0)
|
|
: sourceVariants.length;
|
|
return arrived >= 1;
|
|
}, undefined, { timeout: 240_000 });
|
|
}
|
|
|
|
async function waitForAcceptCleanup(page, tmp) {
|
|
const deadline = Date.now() + 45_000;
|
|
while (Date.now() < deadline) {
|
|
const browserClean = await page.evaluate(() => {
|
|
const query = window.__impeccableLiveQuery || ((selector) => document.querySelector(selector));
|
|
const wrapperGone = !query('[data-impeccable-variants]');
|
|
const state = document.documentElement.dataset.impeccableLiveState;
|
|
return wrapperGone && (!state || state === 'PICKING');
|
|
}).catch(() => false);
|
|
const source = await readFile(join(tmp, SOURCE_FILE), 'utf-8').catch(() => '');
|
|
const sourceClean = source && !/data-impeccable-|impeccable-(?:variants|carbonize|params|original)/i.test(source);
|
|
if (browserClean && sourceClean) return true;
|
|
await new Promise((resolvePromise) => setTimeout(resolvePromise, 40));
|
|
}
|
|
return false;
|
|
}
|
|
|
|
async function verifyBuild(tmp) {
|
|
const startedAt = performance.now();
|
|
try {
|
|
await execFileP('npm', ['run', 'build'], { cwd: tmp, timeout: 120_000, maxBuffer: 4 * 1024 * 1024 });
|
|
return { passed: true, durationMs: roundMs(performance.now() - startedAt) };
|
|
} catch (error) {
|
|
return {
|
|
passed: false,
|
|
durationMs: roundMs(performance.now() - startedAt),
|
|
error: String(error?.stderr || error?.message || error).slice(0, 2000),
|
|
};
|
|
}
|
|
}
|
|
|
|
function evaluateStrategies(groups) {
|
|
const evaluations = [];
|
|
for (const provider of new Set(groups.map((group) => group.provider))) {
|
|
const providerGroups = groups.filter((group) => group.provider === provider);
|
|
const baseline = providerGroups.find((group) => group.strategy === 'atomic-full');
|
|
const baselineValid = baseline?.summary.gatePassRate === 1
|
|
&& Number.isFinite(baseline?.summary.metrics.firstReviewableMs?.median);
|
|
for (const group of providerGroups) {
|
|
const summary = group.summary;
|
|
const qualityPass = summary.gatePassRate === 1 && summary.cleanupPassRate === 1;
|
|
const first = summary.metrics.firstReviewableMs?.median;
|
|
const baselineFirst = baseline?.summary.metrics.firstReviewableMs?.median;
|
|
const firstImprovement = Number.isFinite(first) && Number.isFinite(baselineFirst) && baselineFirst > 0
|
|
? Number((1 - first / baselineFirst).toFixed(4))
|
|
: null;
|
|
const latencyPass = group.strategy === 'atomic-full'
|
|
|| (baselineValid ? firstImprovement != null && firstImprovement > 0.1 : Number.isFinite(first) && first < 15_000);
|
|
evaluations.push({
|
|
provider,
|
|
model: group.model,
|
|
strategy: group.strategy,
|
|
decision: qualityPass && latencyPass ? 'accept' : 'reject',
|
|
firstReviewableImprovementVsAtomic: firstImprovement,
|
|
qualityPass,
|
|
latencyPass,
|
|
reason: !qualityPass
|
|
? 'Rejected: fidelity, source validity, or cleanup gate failed.'
|
|
: !latencyPass
|
|
? 'Rejected: first-reviewable median did not improve by more than 10%.'
|
|
: group.strategy === 'atomic-full'
|
|
? 'Control: retained as the one-call baseline.'
|
|
: !baselineValid
|
|
? 'Accepted: quality passed and first review completed under 15 seconds; the atomic control was invalid for this provider.'
|
|
: 'Accepted: materially faster first review with all quality and cleanup gates intact.',
|
|
});
|
|
}
|
|
}
|
|
return evaluations;
|
|
}
|
|
|
|
function publicProviderSelection(item) {
|
|
return {
|
|
provider: item.provider,
|
|
label: item.label,
|
|
model: item.model,
|
|
keyPresent: item.keyPresent,
|
|
pricePerMillion: item.pricePerMillion,
|
|
effort: item.effort,
|
|
priceSource: item.priceSource,
|
|
};
|
|
}
|
|
|
|
function publicProviderRecord(record) {
|
|
return {
|
|
phase: record.phase,
|
|
lane: record.lane,
|
|
attempt: record.attempt,
|
|
durationMs: record.durationMs,
|
|
totalPhaseMs: record.totalPhaseMs,
|
|
usage: record.usage,
|
|
estimatedCostUsd: record.estimatedCostUsd,
|
|
error: record.error,
|
|
outputScore: record.output ? scoreVariantOutput(record.output) : undefined,
|
|
};
|
|
}
|
|
|
|
function qualityGateDescription() {
|
|
return {
|
|
deterministic: true,
|
|
pass: 'overall >= 0.90 and every dimension >= 0.75; accepted source must build and contain no Live markers',
|
|
dimensions: ['brandFidelity', 'componentFidelity', 'tokenFidelity', 'copyFidelity', 'sourceValidity', 'acceptCleanup'],
|
|
identityLock: BRAND_CONTRACT.identity,
|
|
};
|
|
}
|
|
|
|
function syntheticEvent(id) {
|
|
const outerHTML = BRAND_CONTRACT.sourceExcerpt
|
|
.replaceAll('className=', 'class=')
|
|
.replace(/\s+/g, ' ')
|
|
.trim();
|
|
return {
|
|
id,
|
|
action: 'impeccable',
|
|
freeformPrompt: 'Make this offer easier to scan while staying unmistakably inside the existing brand and component system.',
|
|
count: 3,
|
|
mode: 'replace',
|
|
element: {
|
|
outerHTML,
|
|
tagName: 'ARTICLE',
|
|
className: 'offer-card',
|
|
classes: ['offer-card'],
|
|
textContent: BRAND_CONTRACT.requiredCopy.join(' '),
|
|
},
|
|
};
|
|
}
|
|
|
|
function callsPerStrategy(strategy) {
|
|
if (strategy === 'atomic-full') return 1;
|
|
if (strategy === 'parallel-compact') return 3;
|
|
return 2;
|
|
}
|
|
|
|
function validateConfiguration({ fixture: loadedFixture, strategies: selectedStrategies, selection: selectedProviders, liveSpec: loadedLiveSpec }) {
|
|
if (!loadedFixture.runtime?.pickSelector) throw new Error('benchmark fixture requires runtime.pickSelector');
|
|
if (!loadedLiveSpec.includes('Phase A: Extract the identity')) throw new Error('live.md identity-lock guidance not found');
|
|
for (const strategy of selectedStrategies) if (!STRATEGIES[strategy]) throw new Error(`unknown strategy ${strategy}`);
|
|
if (selectedProviders.length === 0) throw new Error('at least one provider is required');
|
|
for (const provider of selectedProviders) if (!PROVIDER_PROFILES[provider.provider]) throw new Error(`unknown provider ${provider.provider}`);
|
|
}
|
|
|
|
async function persist(report, file) {
|
|
await mkdir(dirname(file), { recursive: true });
|
|
await writeFile(file, JSON.stringify(report, null, 2) + '\n', 'utf-8');
|
|
process.stderr.write(`[live-provider-bench] wrote ${file}\n`);
|
|
}
|
|
|
|
function csv(value) {
|
|
return String(value).split(',').map((item) => item.trim()).filter(Boolean);
|
|
}
|
|
|
|
function roundMs(value) {
|
|
return Number(Number(value).toFixed(2));
|
|
}
|
|
|
|
function roundUsd(value) {
|
|
return Number(Number(value).toFixed(6));
|
|
}
|