SPB Git

spb/autoscholar

Public

QA: execution evidence accepts platform reproduction or tolerant experiment logs; reported values may live in results reports; soft reproduction gate; admin re-evaluate action

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Simon-Pierre Boucher committed (Oct 30, 2026) parent 87b4e55 Browse files

3 changed files +93 −15

added apps/web/src/app/api/admin/quality/[runId]/reevaluate/route.ts +25 −0
@@ -0,0 +1,25 @@
1 +import { audit, transition } from "@autoscholar/core";
2 +import { handler, json } from "@/lib/server/api";
3 +import { db } from "@/lib/server/db";
4 +import { requireAdmin } from "@/lib/server/session";
5 +
6 +export const runtime = "nodejs";
7 +
8 +/** Re-runs the deterministic quality gates on the existing artifacts (e.g. after a gate policy fix). */
9 +export const POST = handler(async (req, { params }) => {
10 + const sess = await requireAdmin(req, { mutation: true });
11 + const { runId } = (await params) as { runId: string };
12 + const run = await db().transaction(async (tx) => {
13 + const r = await transition(tx, runId, "quality_check", {
14 + actor: `admin:${sess.userId}`,
15 + reason: "re-evaluation requested",
16 + expectFrom: ["quality_failed"],
17 + });
18 + await audit(tx, { type: "admin", id: sess.userId, ip: sess.ip }, "quality.reevaluate", {
19 + type: "run",
20 + id: runId,
21 + });
22 + return r;
23 + });
24 + return json({ ok: true, status: run.status });
25 +});
modified packages/schemas/src/run.ts +1 −1
@@ -29,7 +29,7 @@ export const RUN_TRANSITIONS: Record<RunStatus, readonly RunStatus[]> = {
29 29 provisioning: ["running", "quality_check", "retrying", "cancelling", "cancelled", "failed"],
30 30 running: ["quality_check", "retrying", "cancelling", "failed", "revising"],
31 31 quality_check: ["quality_failed", "ready_to_publish", "cancelling", "failed"],
32 − quality_failed: ["revising", "cancelled", "failed", "ready_to_publish"],
32 + quality_failed: ["revising", "quality_check", "cancelled", "failed", "ready_to_publish"],
33 33 revising: ["queued", "retrying", "cancelling", "failed"],
34 34 ready_to_publish: ["awaiting_approval", "publishing", "cancelled", "failed"],
35 35 awaiting_approval: ["publishing", "cancelled", "quality_failed"],
modified services/qa/src/gates.ts +67 −14
@@ -370,7 +370,12 @@ export async function runGates(ctx: GateContext): Promise<GateResult[]> {
370 370 out.push(fail("results.consistency", "hard", `metrics.json invalid: ${metrics.error}`));
371 371 else {
372 372 const rv = metrics.value.reportedValues;
373 − const missing = rv.filter((v) => !texContainsNumber(tex, v.value));
373 + const reportTexts: string[] = [];
374 + for (const a of ctx.artifacts)
375 + if (/^results\/.*\.(md|tex)$/.test(a.path) && a.bytes < 4 * 1024 * 1024)
376 + reportTexts.push((await a.read()).toString("utf8"));
377 + const corpus = `${tex}\n${reportTexts.join("\n")}`;
378 + const missing = rv.filter((v) => !texContainsNumber(corpus, v.value));
374 379 out.push(
375 380 rv.length >= 3 && missing.length === 0
376 381 ? pass(
@@ -390,33 +395,81 @@ export async function runGates(ctx: GateContext): Promise<GateResult[]> {
390 395 if (metrics.value.synthetic && ctx.mode === "empirical")
391 396 out.push(fail("results.real", "hard", "empirical results flagged synthetic"));
392 397 }
398 + // execution evidence: platform reproduction (strongest), then the experiment log (tolerant of the agent's log shape)
399 + const repro = await readJson(
400 + find(ctx, "results/reproduction.json"),
401 + (x) =>
402 + x as {
403 + reproduceScript?: { ran?: boolean; exitCode?: number | null };
404 + metricsCompared?: number;
405 + metricsMismatched?: unknown[];
406 + metricsMissingInRerun?: number;
407 + },
408 + );
409 + const reproOk = repro.value?.reproduceScript?.ran === true && repro.value.reproduceScript.exitCode === 0;
393 410 const logA = find(ctx, P.experimentLog);
394 − const entries = logA
411 + const rawEntries = logA
395 412 ? (await logA.read())
396 413 .toString("utf8")
397 414 .split(/\r?\n/)
398 415 .filter(Boolean)
399 416 .map((l) => {
400 417 try {
401 − return ExperimentLogEntry.parse(JSON.parse(l));
418 + return JSON.parse(l) as Record<string, unknown>;
402 419 } catch {
403 420 return null;
404 421 }
405 422 })
423 + .filter((e): e is Record<string, unknown> => !!e)
406 424 : [];
407 − const ok = entries.filter((e) => e && e.exitCode === 0).length;
425 + const okEntries = rawEntries.filter((e) => {
426 + const strict = ExperimentLogEntry.safeParse(e);
427 + if (strict.success) return strict.data.exitCode === 0;
428 + const code = e.exitCode ?? e.exit_code ?? e.returncode ?? e.status;
429 + if (code !== undefined) return code === 0 || code === "0" || code === "ok" || code === "success";
430 + return !!(e.script || e.command || e.name) && !(e.error || e.failed === true);
431 + }).length;
408 432 const codeFiles = ctx.artifacts.filter(
409 − (a) => a.path.startsWith("code/") && a.path !== P.codeReadme,
433 + (a) => a.path.startsWith("code/") && a.path !== P.codeReadme && !/\.(md|txt)$/.test(a.path),
410 434 ).length;
411 − out.push(
412 − ok >= 1 && codeFiles >= 1
413 − ? pass("execution.evidence", "hard", `${ok} successful executions logged, ${codeFiles} code files`)
414 − : fail(
415 − "execution.evidence",
416 − "hard",
417 − `insufficient execution evidence (successful runs: ${ok}, code files: ${codeFiles})`,
418 − ),
419 − );
435 + const mismatched = repro.value?.metricsMismatched?.length ?? 0;
436 + if ((reproOk || okEntries >= 1) && codeFiles >= 1) {
437 + out.push(
438 + pass(
439 + "execution.evidence",
440 + "hard",
441 + `${reproOk ? "platform reproduction succeeded; " : ""}${okEntries} logged execution(s), ${codeFiles} code files`,
442 + { reproOk, okEntries, codeFiles, metricsCompared: repro.value?.metricsCompared, mismatched },
443 + ),
444 + );
445 + } else {
446 + out.push(
447 + fail(
448 + "execution.evidence",
449 + "hard",
450 + `insufficient execution evidence (platform reproduction ok: ${reproOk}, logged successful executions: ${okEntries}, code files: ${codeFiles})`,
451 + { reproOk, okEntries, codeFiles },
452 + ),
453 + );
454 + }
455 + if (repro.value?.reproduceScript?.ran) {
456 + out.push(
457 + reproOk && mismatched === 0
458 + ? pass(
459 + "reproduction.clean",
460 + "soft",
461 + `reproduce.sh re-ran cleanly; ${repro.value.metricsCompared ?? 0} metrics compared, 0 mismatches`,
462 + )
463 + : fail(
464 + "reproduction.clean",
465 + "soft",
466 + reproOk
467 + ? `${mismatched} metric(s) differed on re-execution`
468 + : `reproduce.sh exited with ${repro.value.reproduceScript.exitCode}`,
469 + { mismatched: repro.value.metricsMismatched?.slice(0, 10) },
470 + ),
471 + );
472 + }
420 473 } else out.push(skip("results.consistency", "hard", "not required for this mode"));
421 474
422 475 // G11 data provenance
423 476
Menu

Theme