mirror of
https://github.com/proffesor-for-testing/agentic-qe.git
synced 2026-09-19 08:45:47 +08:00
af4140efb9
Apply Phoenix Architecture principles to AQE — regenerate internals, conserve the interface. ADR-113 (evals are oracles, durable-first tests, mutation-score gate): - src/validation/oracle-eval.ts grades a generated test by running it against a reference impl + operator mutants (reuses qe-arena, ADR-104); rejects assertion-less output - oracle mode wired into scripts/run-skill-eval.ts (skips under the simulating runner; runs live) - qe-test-generation rewritten durable-first; its eval flipped to oracle - src/feedback/regenerability-gate.ts: mutation-score + regenerability gate (warn-default, opt-in block) - live-validated: Claude sonnet-4-6 clears the oracle 5/5; weak local-model output correctly fails ADR-114 (conservation layer, Phoenix essay 14): - src/validation/conservation-guard.ts + scripts/conservation-guard.ts guard CLI commands, output schemas, and dashboard API against breaking removals (deprecation registry exempts) - policy doc + baselines in verification/conservation/; wired into invariant-check.yml non-blocking LLM provider fixes: - OpenRouterProvider.type was 'openai' but registered under 'openrouter' -> ProviderManager failover never found it; fixed to 'openrouter' end-to-end - Gemini default model gemini-1.5-pro was retired -> gemini-2.5-flash; models/pricing refreshed from the live Gemini ListModels API - scripts/provider-health.ts (npm run providers:health) Cheaper-model eval lanes: - scripts/oracle-model-bench.ts + openrouter-models.ts; openai/gpt-oss-120b clears the oracle ~50-70x cheaper than Claude Sonnet; guide in docs/guides/cheaper-model-eval-lanes.md Verification: npm run build ok; 115 new+touched tests pass; 724 LLM tests pass; lint + parity clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
124 lines
5.1 KiB
TypeScript
124 lines
5.1 KiB
TypeScript
#!/usr/bin/env tsx
|
|
/**
|
|
* Live oracle validation (ADR-113, P5).
|
|
*
|
|
* Closes the loop end-to-end with a REAL model: a provider generates a test for
|
|
* a reference function, then the oracle grades that generated test by running it
|
|
* against the reference (must pass) and its operator mutants (must kill). No
|
|
* simulation — this is the production path the eval runner takes once a provider
|
|
* is configured.
|
|
*
|
|
* # Claude (frontier) lane — needs ANTHROPIC_API_KEY (e.g. node --env-file=.env):
|
|
* EVAL_PROVIDER=claude EVAL_MODEL=claude-sonnet-4-6 tsx scripts/validate-live-oracle.ts
|
|
* # Local Ollama lane:
|
|
* EVAL_PROVIDER=ollama OLLAMA_MODEL='qwen3:30b-a3b' tsx scripts/validate-live-oracle.ts
|
|
*/
|
|
|
|
import { readFileSync, existsSync } from 'fs';
|
|
import { createProviderManager } from '../src/shared/llm/provider-manager.js';
|
|
import type { ProviderManager } from '../src/shared/llm/provider-manager.js';
|
|
import type { LLMProviderType } from '../src/shared/llm/interfaces.js';
|
|
import { buildTestGenPrompt } from '../src/validation/test-gen-prompt.js';
|
|
import { SkillEvaluationRunner } from './run-skill-eval.js';
|
|
|
|
// Load .env (KEY=VALUE lines) into process.env without adding a dependency, so
|
|
// `npm run eval:live:claude` finds ANTHROPIC_API_KEY. Existing env wins; never logged.
|
|
if (existsSync('.env')) {
|
|
for (const line of readFileSync('.env', 'utf8').split('\n')) {
|
|
const m = /^\s*([A-Z_][A-Z0-9_]*)\s*=\s*(.*)\s*$/.exec(line);
|
|
if (m && !process.env[m[1]]) process.env[m[1]] = m[2].replace(/^["']|["']$/g, '');
|
|
}
|
|
}
|
|
|
|
const PROVIDER = (process.env.EVAL_PROVIDER ?? 'ollama') as LLMProviderType;
|
|
const BASE_URL = process.env.OLLAMA_BASE_URL ?? 'http://host.docker.internal:11434';
|
|
const MODEL =
|
|
process.env.EVAL_MODEL ??
|
|
(PROVIDER === 'claude' ? 'claude-sonnet-4-6' : process.env.OLLAMA_MODEL ?? 'qwen3:30b-a3b');
|
|
|
|
const REFERENCE_IMPL = `export function classify(score, bonus) {
|
|
const total = score + bonus;
|
|
if (total >= 90 && bonus > 0) return 'A';
|
|
if (total >= 70) return 'B';
|
|
return 'C';
|
|
}
|
|
`;
|
|
|
|
/** Eval runner whose "skill output" comes from a real model call. */
|
|
class LiveSkillEvaluationRunner extends SkillEvaluationRunner {
|
|
constructor(suite: unknown, private readonly pm: ProviderManager) {
|
|
super(suite as never, true);
|
|
}
|
|
protected isLive(): boolean {
|
|
return true;
|
|
}
|
|
protected async produceSkillOutput(testCase: { oracle?: { reference_impl?: string; module_name?: string }; input?: { code?: string } }): Promise<string> {
|
|
const code = testCase.oracle?.reference_impl ?? testCase.input?.code ?? '';
|
|
const moduleName = testCase.oracle?.module_name ?? 'module';
|
|
const res = await this.pm.generate(buildTestGenPrompt(code, moduleName), {
|
|
preferredProvider: PROVIDER,
|
|
temperature: 0.2,
|
|
maxTokens: 1500,
|
|
timeoutMs: 180_000,
|
|
skipCache: true,
|
|
});
|
|
return res.content;
|
|
}
|
|
}
|
|
|
|
function oracleSuite() {
|
|
return {
|
|
skill: 'live-oracle-validation',
|
|
version: '1.0.0',
|
|
models_to_test: [MODEL],
|
|
test_cases: [
|
|
{
|
|
id: 'tc_live_oracle',
|
|
description: 'model-generated test must kill mutants of classify()',
|
|
priority: 'critical',
|
|
input: { code: REFERENCE_IMPL },
|
|
expected_output: {},
|
|
validation: { oracle: true },
|
|
oracle: { module_name: 'classify', reference_impl: REFERENCE_IMPL, threshold: 0.6 },
|
|
},
|
|
],
|
|
success_criteria: { pass_rate: 1.0, critical_pass_rate: 1.0 },
|
|
};
|
|
}
|
|
|
|
async function main(): Promise<void> {
|
|
const target = PROVIDER === 'claude' ? `${MODEL} (Anthropic API)` : `${MODEL} @ ${BASE_URL}`;
|
|
console.log(`Live oracle validation → ${target}\n`);
|
|
if (PROVIDER === 'claude' && !process.env.ANTHROPIC_API_KEY) {
|
|
console.error('EVAL_PROVIDER=claude requires ANTHROPIC_API_KEY (try: node --env-file=.env ...).');
|
|
process.exit(1);
|
|
}
|
|
const pm = createProviderManager(
|
|
PROVIDER === 'claude'
|
|
? { primary: 'claude', fallbacks: [], providers: { claude: { model: MODEL, maxTokens: 1500, temperature: 0.2 } } }
|
|
: { primary: 'ollama', fallbacks: [], providers: { ollama: { model: MODEL, baseUrl: BASE_URL, maxTokens: 1500, temperature: 0.2 } } },
|
|
);
|
|
|
|
const runner = new LiveSkillEvaluationRunner(oracleSuite(), pm);
|
|
const result = await runner.runForModel(MODEL);
|
|
const tc = result.test_results[0];
|
|
|
|
console.log('\n========== LIVE ORACLE RESULT ==========');
|
|
console.log(`baseline passed (test runs against real impl): ${tc.oracle_baseline_passed}`);
|
|
console.log(`mutants killed: ${tc.mutants_killed}/${tc.mutants_total}`);
|
|
console.log(`mutation score: ${((tc.mutation_score ?? 0) * 100).toFixed(0)}%`);
|
|
console.log(`survived mutants: ${(tc.survived_mutant_ids ?? []).join(', ') || 'none'}`);
|
|
console.log(`oracle verdict: ${tc.passed ? 'PASS' : 'FAIL'}`);
|
|
console.log('========================================');
|
|
if (tc.raw_output) {
|
|
console.log('\n--- model-generated test (first 1200 chars) ---');
|
|
console.log(tc.raw_output.slice(0, 1200));
|
|
}
|
|
process.exit(tc.oracle_baseline_passed ? 0 : 2);
|
|
}
|
|
|
|
main().catch((e) => {
|
|
console.error('live validation error:', e instanceof Error ? e.message : String(e));
|
|
process.exit(1);
|
|
});
|