proof-harness 0.1.0-next.13 → 0.1.0-next.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/COMPATIBILITY.md +83 -0
- package/README.md +81 -0
- package/cli/config.mjs +1 -0
- package/cli/engines/proof_coverage.mjs +8 -1
- package/cli/engines/proof_execution_reporter.mjs +65 -0
- package/cli/engines/proof_inventory_build.mjs +136 -0
- package/cli/engines/proof_mutation_check.mjs +160 -141
- package/cli/engines/proof_parallel.mjs +943 -0
- package/cli/engines/proof_parallel_contract.mjs +167 -0
- package/cli/engines/proof_verify.mjs +42 -7
- package/cli/proof-harness.mjs +9 -0
- package/dist/playwright.js +70 -39
- package/dist/playwright.js.map +1 -1
- package/package.json +1 -1
- package/proof-consumer-fixtures/parallel/contract.json +46 -0
|
@@ -4,11 +4,8 @@ import {
|
|
|
4
4
|
validateTableAcl,
|
|
5
5
|
restoreTableAclSql,
|
|
6
6
|
} from "./proof_table_acl.mjs";
|
|
7
|
-
import {
|
|
8
|
-
|
|
9
|
-
assertionSelectorsTurnedRed,
|
|
10
|
-
} from "./proof_control_sensitivity.mjs";
|
|
11
|
-
import { readTraceDirectory } from "../../dist/node.js";
|
|
7
|
+
import { assertionSelectorsTurnedRed } from "./proof_control_sensitivity.mjs";
|
|
8
|
+
import { readTraceDirectory, sourceHash } from "../../dist/node.js";
|
|
12
9
|
// ---------------------------------------------------------------------------
|
|
13
10
|
// proof_mutation_check.mjs
|
|
14
11
|
//
|
|
@@ -47,11 +44,7 @@ import path from "node:path";
|
|
|
47
44
|
import { createRequire } from "node:module";
|
|
48
45
|
import { fileURLToPath } from "node:url";
|
|
49
46
|
import nextEnv from "@next/env";
|
|
50
|
-
import {
|
|
51
|
-
deriveAutomaticRlsMutations,
|
|
52
|
-
evaluateMutationClaimCoverage,
|
|
53
|
-
mutationCoversClaim,
|
|
54
|
-
} from "./proof_mutation_inventory.mjs";
|
|
47
|
+
import { buildMutationInventory } from "./proof_inventory_build.mjs";
|
|
55
48
|
import {
|
|
56
49
|
assessPlantedSubject,
|
|
57
50
|
assessSubjectAfterProof,
|
|
@@ -77,10 +70,8 @@ loadEnvConfig(CONFIG.rootDir);
|
|
|
77
70
|
const require = createRequire(import.meta.url);
|
|
78
71
|
|
|
79
72
|
const CONFIG_TOML = CONFIG.repository.supabaseConfig;
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
const MUTATION_POLICY_PATH = CONFIG.policies.mutation;
|
|
83
|
-
const MUTATION_ARTIFACT_DIR = CONTROL_SENSITIVITY
|
|
73
|
+
let TRACE_DIR = CONFIG.artifacts.traces;
|
|
74
|
+
let MUTATION_ARTIFACT_DIR = CONTROL_SENSITIVITY
|
|
84
75
|
? CONFIG.artifacts.controlSensitivity
|
|
85
76
|
: CONFIG.artifacts.mutations;
|
|
86
77
|
const RECOVERY_FILE = path.join(
|
|
@@ -400,113 +391,12 @@ const PROOFS_DIR = CONFIG.roots.proofs;
|
|
|
400
391
|
function listSpecs() {
|
|
401
392
|
if (!fs.existsSync(PROOFS_DIR)) return [];
|
|
402
393
|
return fs
|
|
403
|
-
.readdirSync(PROOFS_DIR)
|
|
394
|
+
.readdirSync(PROOFS_DIR, { recursive: true })
|
|
404
395
|
.filter((f) => f.endsWith(".proof.ts"))
|
|
405
396
|
.map((f) => path.join(PROOFS_DIR, f))
|
|
406
397
|
.sort();
|
|
407
398
|
}
|
|
408
399
|
|
|
409
|
-
function readMutationPolicy() {
|
|
410
|
-
try {
|
|
411
|
-
const policy = JSON.parse(fs.readFileSync(MUTATION_POLICY_PATH, "utf8"));
|
|
412
|
-
if (policy.schemaVersion !== 1) {
|
|
413
|
-
return {
|
|
414
|
-
policy: { acceptedClaims: [] },
|
|
415
|
-
problems: [
|
|
416
|
-
`${MUTATION_POLICY_PATH} has unsupported schemaVersion ${policy.schemaVersion ?? "<missing>"}; expected 1`,
|
|
417
|
-
],
|
|
418
|
-
};
|
|
419
|
-
}
|
|
420
|
-
return { policy, problems: [] };
|
|
421
|
-
} catch (error) {
|
|
422
|
-
return {
|
|
423
|
-
policy: { acceptedClaims: [] },
|
|
424
|
-
problems: [
|
|
425
|
-
`cannot read ${MUTATION_POLICY_PATH}: ${error instanceof Error ? error.message : String(error)}`,
|
|
426
|
-
],
|
|
427
|
-
};
|
|
428
|
-
}
|
|
429
|
-
}
|
|
430
|
-
|
|
431
|
-
function buildMutationInventory() {
|
|
432
|
-
if (CONTROL_SENSITIVITY) {
|
|
433
|
-
if (!CONFIG.controlSensitivityCatalog)
|
|
434
|
-
throw new Error(
|
|
435
|
-
"[PROOF_FAIL] control_contract: configure a separate controlSensitivityCatalog",
|
|
436
|
-
);
|
|
437
|
-
const traces = readTraceDirectory(TRACE_DIR, {
|
|
438
|
-
rootDir: CONFIG.rootDir,
|
|
439
|
-
excludedPaths: evidenceExclusions(CONFIG),
|
|
440
|
-
});
|
|
441
|
-
const ids = new Set();
|
|
442
|
-
const mutations = MUTATIONS.map((m) => {
|
|
443
|
-
if (ids.has(m.id)) throw new Error("duplicate control-sensitivity id");
|
|
444
|
-
ids.add(m.id);
|
|
445
|
-
return { ...m, resolvedControls: validateControlBaseline(m, traces) };
|
|
446
|
-
});
|
|
447
|
-
return {
|
|
448
|
-
mutations,
|
|
449
|
-
problems: [],
|
|
450
|
-
claims: [],
|
|
451
|
-
acceptedClaims: [],
|
|
452
|
-
uncoveredClaims: [],
|
|
453
|
-
uncoveredActionClaims: [],
|
|
454
|
-
};
|
|
455
|
-
}
|
|
456
|
-
const derived = deriveAutomaticRlsMutations({
|
|
457
|
-
tracesDir: TRACE_DIR,
|
|
458
|
-
excludedPaths: evidenceExclusions(CONFIG),
|
|
459
|
-
schemaPath: SCHEMA_PATH,
|
|
460
|
-
explicitMutations: MUTATIONS,
|
|
461
|
-
});
|
|
462
|
-
const mutations = [...MUTATIONS, ...derived.mutations];
|
|
463
|
-
const ids = new Set();
|
|
464
|
-
const problems = [...derived.problems];
|
|
465
|
-
|
|
466
|
-
for (const mutation of mutations) {
|
|
467
|
-
if (ids.has(mutation.id)) {
|
|
468
|
-
problems.push(`duplicate mutation id "${mutation.id}"`);
|
|
469
|
-
}
|
|
470
|
-
ids.add(mutation.id);
|
|
471
|
-
}
|
|
472
|
-
|
|
473
|
-
const loadedPolicy = readMutationPolicy();
|
|
474
|
-
const claimCoverage = evaluateMutationClaimCoverage({
|
|
475
|
-
claims: derived.claims,
|
|
476
|
-
evidence: derived.evidence,
|
|
477
|
-
mutations,
|
|
478
|
-
policy: loadedPolicy.policy,
|
|
479
|
-
});
|
|
480
|
-
problems.push(...loadedPolicy.problems, ...claimCoverage.problems);
|
|
481
|
-
for (const claim of claimCoverage.uncoveredClaims) {
|
|
482
|
-
problems.push(
|
|
483
|
-
`${claim.spec} has no mutation or accepted policy entry for ${claim.kind}:${claim.target}:${claim.operation}`,
|
|
484
|
-
);
|
|
485
|
-
}
|
|
486
|
-
|
|
487
|
-
const mutationsWithClaims = mutations.map((mutation) => ({
|
|
488
|
-
...mutation,
|
|
489
|
-
resolvedClaims: derived.claims
|
|
490
|
-
.filter((claim) => mutationCoversClaim(mutation, claim))
|
|
491
|
-
.map((claim) => ({
|
|
492
|
-
kind: claim.kind,
|
|
493
|
-
target: claim.target,
|
|
494
|
-
operation: claim.operation,
|
|
495
|
-
})),
|
|
496
|
-
}));
|
|
497
|
-
|
|
498
|
-
return {
|
|
499
|
-
mutations: mutationsWithClaims,
|
|
500
|
-
problems,
|
|
501
|
-
claims: derived.claims,
|
|
502
|
-
acceptedClaims: claimCoverage.acceptedClaims,
|
|
503
|
-
uncoveredClaims: claimCoverage.uncoveredClaims,
|
|
504
|
-
uncoveredActionClaims: claimCoverage.uncoveredClaims.filter((claim) =>
|
|
505
|
-
/^[A-Za-z][\w-]*:[A-Za-z][\w-]*$/.test(claim.target),
|
|
506
|
-
),
|
|
507
|
-
};
|
|
508
|
-
}
|
|
509
|
-
|
|
510
400
|
/**
|
|
511
401
|
* Returns inventory problems plus mutations pointing at specs that no longer
|
|
512
402
|
* exist. Claim-level mutation/policy coverage is computed in buildMutationInventory.
|
|
@@ -649,6 +539,10 @@ function waitForExit(child, timeoutMs) {
|
|
|
649
539
|
*/
|
|
650
540
|
async function sharedDevServer() {
|
|
651
541
|
if (await serverIsReachable(DEV_SERVER_URL)) {
|
|
542
|
+
if (process.env.PROOF_REQUIRE_OWNED_SERVER === "true")
|
|
543
|
+
throw new Error(
|
|
544
|
+
"parallel mutations require a fresh runner-owned server; stop the existing process first",
|
|
545
|
+
);
|
|
652
546
|
const issue = await proofServerIssue(DEV_SERVER_URL);
|
|
653
547
|
if (issue) {
|
|
654
548
|
throw new Error(
|
|
@@ -844,7 +738,7 @@ function writeMutationSummary(results, inventory) {
|
|
|
844
738
|
);
|
|
845
739
|
}
|
|
846
740
|
|
|
847
|
-
function runProof(spec, mutationId, traceDirectory) {
|
|
741
|
+
function runProof(spec, mutationId, traceDirectory, executionSourceHash) {
|
|
848
742
|
return new Promise((resolve) => {
|
|
849
743
|
const playwrightCli = require.resolve("@playwright/test/cli");
|
|
850
744
|
const child = spawn(
|
|
@@ -853,8 +747,8 @@ function runProof(spec, mutationId, traceDirectory) {
|
|
|
853
747
|
playwrightCli,
|
|
854
748
|
"test",
|
|
855
749
|
"--project=proofs",
|
|
856
|
-
spec,
|
|
857
|
-
|
|
750
|
+
`${spec.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}$`,
|
|
751
|
+
`--reporter=line,${fileURLToPath(new URL("./proof_execution_reporter.mjs", import.meta.url))}`,
|
|
858
752
|
// A planted defect is expected to turn the proof red. Retrying that
|
|
859
753
|
// deliberate failure only reruns the same proof against the same
|
|
860
754
|
// broken database, multiplying mutation time without adding evidence.
|
|
@@ -868,7 +762,15 @@ function runProof(spec, mutationId, traceDirectory) {
|
|
|
868
762
|
CI: process.env.CI ?? "",
|
|
869
763
|
PLAYWRIGHT_REUSE_EXISTING_SERVER: "true",
|
|
870
764
|
PROOF_MUTATION_ID: mutationId,
|
|
765
|
+
...(executionSourceHash
|
|
766
|
+
? { PROOF_SOURCE_HASH: executionSourceHash }
|
|
767
|
+
: {}),
|
|
871
768
|
PROOF_TRACES_DIR: path.resolve(traceDirectory),
|
|
769
|
+
PROOF_EXECUTION_REPORT: path.resolve(
|
|
770
|
+
traceDirectory,
|
|
771
|
+
"../",
|
|
772
|
+
`${mutationId}-execution.json`,
|
|
773
|
+
),
|
|
872
774
|
},
|
|
873
775
|
},
|
|
874
776
|
);
|
|
@@ -895,14 +797,22 @@ function runProof(spec, mutationId, traceDirectory) {
|
|
|
895
797
|
// ---------------------------------------------------------------------------
|
|
896
798
|
|
|
897
799
|
function parseArgs(argv) {
|
|
898
|
-
const args = { inventory: false, list: false };
|
|
800
|
+
const args = { inventory: false, list: false, specs: [] };
|
|
899
801
|
for (let i = 0; i < argv.length; i++) {
|
|
900
802
|
const v = argv[i];
|
|
803
|
+
if (
|
|
804
|
+
["--only", "--spec", "--baseline-dir", "--output-dir"].includes(v) &&
|
|
805
|
+
(!argv[i + 1] || argv[i + 1].startsWith("--"))
|
|
806
|
+
)
|
|
807
|
+
throw new Error(`${v} requires a value`);
|
|
901
808
|
if (v === "--control-sensitivity") continue;
|
|
902
809
|
if (v === "--inventory") args.inventory = true;
|
|
903
810
|
else if (v === "--recover") args.recover = true;
|
|
904
811
|
else if (v === "--list") args.list = true;
|
|
905
812
|
else if (v === "--only") args.only = argv[++i];
|
|
813
|
+
else if (v === "--spec") args.specs.push(argv[++i]);
|
|
814
|
+
else if (v === "--baseline-dir") args.baselineDir = argv[++i];
|
|
815
|
+
else if (v === "--output-dir") args.outputDir = argv[++i];
|
|
906
816
|
else if (v === "--help" || v === "-h") args.help = true;
|
|
907
817
|
else {
|
|
908
818
|
console.error(`[mutation] unknown flag: ${v}`);
|
|
@@ -943,7 +853,33 @@ Both verify planting, run the proof, restore, and fail on incomplete evidence.`)
|
|
|
943
853
|
return;
|
|
944
854
|
}
|
|
945
855
|
|
|
946
|
-
|
|
856
|
+
if (args.baselineDir) TRACE_DIR = path.resolve(args.baselineDir);
|
|
857
|
+
if (args.outputDir) MUTATION_ARTIFACT_DIR = path.resolve(args.outputDir);
|
|
858
|
+
if (args.specs.length && !args.baselineDir)
|
|
859
|
+
throw new Error("--spec requires a fresh --baseline-dir");
|
|
860
|
+
if (args.specs.some((s) => !listSpecs().includes(s)))
|
|
861
|
+
throw new Error("unknown proof spec");
|
|
862
|
+
if (args.specs.length) {
|
|
863
|
+
const traces = readTraceDirectory(TRACE_DIR, {
|
|
864
|
+
rootDir: CONFIG.rootDir,
|
|
865
|
+
excludedPaths: evidenceExclusions(CONFIG),
|
|
866
|
+
});
|
|
867
|
+
if (
|
|
868
|
+
traces.some(
|
|
869
|
+
(t) => t.mutation || !args.specs.includes(t.specFile) || !t.passed,
|
|
870
|
+
)
|
|
871
|
+
)
|
|
872
|
+
throw new Error("scoped baseline contains unexpected or failed traces");
|
|
873
|
+
if (args.specs.some((s) => !traces.some((t) => t.specFile === s)))
|
|
874
|
+
throw new Error("scoped baseline is incomplete");
|
|
875
|
+
}
|
|
876
|
+
const inventory = buildMutationInventory({
|
|
877
|
+
config: CONFIG,
|
|
878
|
+
mutations: MUTATIONS,
|
|
879
|
+
controlSensitivity: CONTROL_SENSITIVITY,
|
|
880
|
+
tracesDir: TRACE_DIR,
|
|
881
|
+
specs: args.specs.length ? args.specs : null,
|
|
882
|
+
});
|
|
947
883
|
const mutations = inventory.mutations;
|
|
948
884
|
|
|
949
885
|
if (args.inventory) {
|
|
@@ -1018,6 +954,15 @@ Both verify planting, run the proof, restore, and fail on incomplete evidence.`)
|
|
|
1018
954
|
...requested.filter((mutation) => isSourceMutation(mutation)),
|
|
1019
955
|
];
|
|
1020
956
|
|
|
957
|
+
if (
|
|
958
|
+
selected.length === 0 &&
|
|
959
|
+
args.specs.length &&
|
|
960
|
+
!args.only &&
|
|
961
|
+
coverageProblems.length === 0
|
|
962
|
+
) {
|
|
963
|
+
writeMutationSummary([], inventory);
|
|
964
|
+
return;
|
|
965
|
+
}
|
|
1021
966
|
if (selected.length === 0) {
|
|
1022
967
|
console.error(
|
|
1023
968
|
`[mutation] no mutation with id "${args.only}"; run --list to see them`,
|
|
@@ -1094,6 +1039,23 @@ Both verify planting, run the proof, restore, and fail on incomplete evidence.`)
|
|
|
1094
1039
|
let snapshot = null;
|
|
1095
1040
|
let applied = false;
|
|
1096
1041
|
let sourceServer = null;
|
|
1042
|
+
const result = {
|
|
1043
|
+
id: m.id,
|
|
1044
|
+
spec: m.spec,
|
|
1045
|
+
outcome: "setup_failure",
|
|
1046
|
+
restoration: "not_needed",
|
|
1047
|
+
cleanupErrors: [],
|
|
1048
|
+
timings: {},
|
|
1049
|
+
};
|
|
1050
|
+
results.push(result);
|
|
1051
|
+
let phase = "snapshot";
|
|
1052
|
+
let phaseStart = performance.now();
|
|
1053
|
+
const stage = (name) => {
|
|
1054
|
+
result.timings[phase] =
|
|
1055
|
+
(result.timings[phase] ?? 0) + performance.now() - phaseStart;
|
|
1056
|
+
phase = name;
|
|
1057
|
+
phaseStart = performance.now();
|
|
1058
|
+
};
|
|
1097
1059
|
try {
|
|
1098
1060
|
handler.preflight?.(container, m.subject);
|
|
1099
1061
|
snapshot = handler.snapshot(container, m.subject);
|
|
@@ -1121,11 +1083,14 @@ Both verify planting, run the proof, restore, and fail on incomplete evidence.`)
|
|
|
1121
1083
|
{ mode: 0o600, flag: "wx" },
|
|
1122
1084
|
);
|
|
1123
1085
|
applied = true;
|
|
1086
|
+
result.restoration = "unknown";
|
|
1087
|
+
stage("plant");
|
|
1124
1088
|
applyMutation(container, m, snapshot);
|
|
1125
1089
|
|
|
1126
1090
|
// A defect that is not demonstrably live proves nothing: running the
|
|
1127
1091
|
// proof anyway would blame the proof ("MISSED") for a plant that never
|
|
1128
1092
|
// happened. Read the subject back and refuse to continue on mismatch.
|
|
1093
|
+
stage("plantReadback");
|
|
1129
1094
|
const plantedSnapshot = handler.snapshot(container, m.subject);
|
|
1130
1095
|
const plantProblem = assessPlantedSubject({
|
|
1131
1096
|
id: m.id,
|
|
@@ -1135,6 +1100,8 @@ Both verify planting, run the proof, restore, and fail on incomplete evidence.`)
|
|
|
1135
1100
|
applyDoesNotChangeSubject: m.applyDoesNotChangeSubject === true,
|
|
1136
1101
|
});
|
|
1137
1102
|
if (plantProblem) throw new Error(plantProblem);
|
|
1103
|
+
result.plantVerified = true;
|
|
1104
|
+
stage("readiness");
|
|
1138
1105
|
|
|
1139
1106
|
if (isSourceMutation(m)) {
|
|
1140
1107
|
if (devServer) {
|
|
@@ -1147,16 +1114,32 @@ Both verify planting, run the proof, restore, and fail on incomplete evidence.`)
|
|
|
1147
1114
|
}
|
|
1148
1115
|
|
|
1149
1116
|
const traceDirectory = path.join(MUTATION_ARTIFACT_DIR, ".runs", m.id);
|
|
1117
|
+
stage("execution");
|
|
1118
|
+
result.executionSourceHash = sourceHash(CONFIG.rootDir, [
|
|
1119
|
+
...evidenceExclusions(CONFIG),
|
|
1120
|
+
MUTATION_ARTIFACT_DIR,
|
|
1121
|
+
TRACE_DIR,
|
|
1122
|
+
]);
|
|
1150
1123
|
const { code, output, timedOut } = await runProof(
|
|
1151
1124
|
m.spec,
|
|
1152
1125
|
m.id,
|
|
1153
1126
|
traceDirectory,
|
|
1127
|
+
result.executionSourceHash,
|
|
1154
1128
|
);
|
|
1129
|
+
fs.writeFileSync(
|
|
1130
|
+
path.join(MUTATION_ARTIFACT_DIR, ".runs", `${m.id}-output.txt`),
|
|
1131
|
+
output,
|
|
1132
|
+
);
|
|
1133
|
+
result.playwrightFailed = code !== 0;
|
|
1134
|
+
result.exitCode = code;
|
|
1135
|
+
result.timedOut = timedOut === true;
|
|
1155
1136
|
if (timedOut)
|
|
1156
1137
|
throw new Error(
|
|
1157
1138
|
"mutation proof timed out; no detection verdict can be issued",
|
|
1158
1139
|
);
|
|
1159
1140
|
if (interrupted) throw new Error("mutation run interrupted");
|
|
1141
|
+
if (!Number.isInteger(code))
|
|
1142
|
+
throw new Error("proof process terminated without an exit status");
|
|
1160
1143
|
const playwrightFailed = code !== 0;
|
|
1161
1144
|
|
|
1162
1145
|
// The defect must still be live now that the proof has finished. If
|
|
@@ -1164,6 +1147,7 @@ Both verify planting, run the proof, restore, and fail on incomplete evidence.`)
|
|
|
1164
1147
|
// re-granting a revoked privilege), the proof's verdict — green or
|
|
1165
1148
|
// red — says nothing about the proof, and archiving it as MISSED
|
|
1166
1149
|
// would misdirect the operator toward a working proof.
|
|
1150
|
+
stage("finalPlantVerification");
|
|
1167
1151
|
const afterProofSnapshot = handler.snapshot(container, m.subject);
|
|
1168
1152
|
const liveProblem = assessSubjectAfterProof({
|
|
1169
1153
|
id: m.id,
|
|
@@ -1174,6 +1158,18 @@ Both verify planting, run the proof, restore, and fail on incomplete evidence.`)
|
|
|
1174
1158
|
applyDoesNotChangeSubject: m.applyDoesNotChangeSubject === true,
|
|
1175
1159
|
});
|
|
1176
1160
|
if (liveProblem) throw new Error(liveProblem);
|
|
1161
|
+
if (
|
|
1162
|
+
result.executionSourceHash &&
|
|
1163
|
+
result.executionSourceHash !==
|
|
1164
|
+
sourceHash(CONFIG.rootDir, [
|
|
1165
|
+
...evidenceExclusions(CONFIG),
|
|
1166
|
+
MUTATION_ARTIFACT_DIR,
|
|
1167
|
+
TRACE_DIR,
|
|
1168
|
+
])
|
|
1169
|
+
)
|
|
1170
|
+
throw new Error("source changed during mutation execution");
|
|
1171
|
+
result.liveVerified = true;
|
|
1172
|
+
stage("evidence");
|
|
1177
1173
|
|
|
1178
1174
|
// A proof can fail for the wrong reason (a crash, a timeout, a missing
|
|
1179
1175
|
// browser). Where the reason matters — as with the deny-all mutation that
|
|
@@ -1190,7 +1186,14 @@ Both verify planting, run the proof, restore, and fail on incomplete evidence.`)
|
|
|
1190
1186
|
? archived.controlRejected
|
|
1191
1187
|
: archived.detected;
|
|
1192
1188
|
|
|
1193
|
-
|
|
1189
|
+
Object.assign(result, {
|
|
1190
|
+
outcome: detected
|
|
1191
|
+
? reasonOk
|
|
1192
|
+
? "detected"
|
|
1193
|
+
: "wrong_reason"
|
|
1194
|
+
: playwrightFailed
|
|
1195
|
+
? "wrong_claim"
|
|
1196
|
+
: "survived",
|
|
1194
1197
|
id: m.id,
|
|
1195
1198
|
detected: archived.detected,
|
|
1196
1199
|
controlRejected: archived.controlRejected,
|
|
@@ -1217,6 +1220,7 @@ Both verify planting, run the proof, restore, and fail on incomplete evidence.`)
|
|
|
1217
1220
|
` ✗ WRONG REASON — the proof failed, but not via "${m.expectFailureContains}".`,
|
|
1218
1221
|
);
|
|
1219
1222
|
} else if (
|
|
1223
|
+
playwrightFailed &&
|
|
1220
1224
|
!(CONTROL_SENSITIVITY
|
|
1221
1225
|
? archived.controlsTurnedRed
|
|
1222
1226
|
: archived.claimsTurnedRed)
|
|
@@ -1230,24 +1234,35 @@ Both verify planting, run the proof, restore, and fail on incomplete evidence.`)
|
|
|
1230
1234
|
);
|
|
1231
1235
|
}
|
|
1232
1236
|
} catch (err) {
|
|
1233
|
-
|
|
1237
|
+
result.error ??= String(err?.message ?? err);
|
|
1238
|
+
result.outcome = result.timedOut
|
|
1239
|
+
? "timeout"
|
|
1240
|
+
: ["plant", "plantReadback", "finalPlantVerification"].includes(phase)
|
|
1241
|
+
? "plant_failure"
|
|
1242
|
+
: phase === "evidence"
|
|
1243
|
+
? "missing_or_invalid_evidence"
|
|
1244
|
+
: "setup_failure";
|
|
1234
1245
|
console.log(` ! ERROR — ${err?.message ?? err}`);
|
|
1235
1246
|
} finally {
|
|
1247
|
+
stage("cleanup");
|
|
1236
1248
|
let stopError = null;
|
|
1237
1249
|
if (sourceServer) {
|
|
1238
1250
|
try {
|
|
1239
1251
|
await sourceServer.stop();
|
|
1240
1252
|
} catch (error) {
|
|
1241
1253
|
stopError = error;
|
|
1254
|
+
result.cleanupErrors.push(String(error?.message ?? error));
|
|
1242
1255
|
}
|
|
1243
1256
|
}
|
|
1244
1257
|
if (applied) {
|
|
1258
|
+
stage("restoration");
|
|
1245
1259
|
try {
|
|
1246
1260
|
// Trust nothing: restore from the exact snapshot and read back.
|
|
1247
1261
|
// Source mutations preserve dirty working-tree contents byte-for-byte;
|
|
1248
1262
|
// database mutations preserve the live object's catalog definition.
|
|
1249
1263
|
restoreMutation(container, m, handler, snapshot);
|
|
1250
1264
|
fs.unlinkSync(RECOVERY_FILE);
|
|
1265
|
+
result.restoration = "verified";
|
|
1251
1266
|
console.log(
|
|
1252
1267
|
` reverted (${handler.describe(m.subject)} verified)\n`,
|
|
1253
1268
|
);
|
|
@@ -1259,16 +1274,30 @@ Both verify planting, run the proof, restore, and fail on incomplete evidence.`)
|
|
|
1259
1274
|
`\n[mutation] FAILED TO REVERT "${m.id}". The local checkout is still vulnerable. ` +
|
|
1260
1275
|
`${recovery}\n${err?.message ?? err}\n`,
|
|
1261
1276
|
);
|
|
1262
|
-
|
|
1277
|
+
result.restoration = "failed";
|
|
1278
|
+
result.cleanupErrors.push(String(err?.message ?? err));
|
|
1279
|
+
result.error ??= String(err?.message ?? err);
|
|
1263
1280
|
}
|
|
1264
1281
|
}
|
|
1282
|
+
stage("complete");
|
|
1283
|
+
writeMutationSummary(results, inventory);
|
|
1284
|
+
if (result.restoration === "failed")
|
|
1285
|
+
throw new Error("restoration failed; recovery journal retained");
|
|
1265
1286
|
if (stopError) throw stopError;
|
|
1266
1287
|
}
|
|
1267
1288
|
}
|
|
1268
1289
|
} finally {
|
|
1269
1290
|
process.removeListener("SIGINT", onSignal);
|
|
1270
1291
|
process.removeListener("SIGTERM", onSignal);
|
|
1271
|
-
|
|
1292
|
+
try {
|
|
1293
|
+
if (devServer) await devServer.stop();
|
|
1294
|
+
} catch (error) {
|
|
1295
|
+
if (results.length)
|
|
1296
|
+
results.at(-1).cleanupErrors.push(String(error?.message ?? error));
|
|
1297
|
+
throw error;
|
|
1298
|
+
} finally {
|
|
1299
|
+
writeMutationSummary(results, inventory);
|
|
1300
|
+
}
|
|
1272
1301
|
}
|
|
1273
1302
|
|
|
1274
1303
|
writeMutationSummary(results, inventory);
|
|
@@ -1276,6 +1305,8 @@ Both verify planting, run the proof, restore, and fail on incomplete evidence.`)
|
|
|
1276
1305
|
const missed = results.filter(
|
|
1277
1306
|
(r) =>
|
|
1278
1307
|
r.error ||
|
|
1308
|
+
r.restoration !== "verified" ||
|
|
1309
|
+
r.cleanupErrors.length > 0 ||
|
|
1279
1310
|
!(CONTROL_SENSITIVITY ? r.controlRejected : r.detected) ||
|
|
1280
1311
|
!r.reasonOk,
|
|
1281
1312
|
);
|
|
@@ -1285,28 +1316,16 @@ Both verify planting, run the proof, restore, and fail on incomplete evidence.`)
|
|
|
1285
1316
|
|
|
1286
1317
|
console.log("── summary ─────────────────────────────────────────");
|
|
1287
1318
|
for (const r of results) {
|
|
1288
|
-
const verdict =
|
|
1289
|
-
? r.error
|
|
1290
|
-
|
|
1291
|
-
: r.controlRejected && r.reasonOk
|
|
1292
|
-
? "CONTROL REJECTED"
|
|
1293
|
-
: "CONTROL NOT REJECTED"
|
|
1294
|
-
: r.error
|
|
1295
|
-
? `ERROR (${r.error.split("\n")[0]})`
|
|
1296
|
-
: r.claimsTurnedRed === false
|
|
1297
|
-
? "WRONG CLAIM"
|
|
1298
|
-
: !r.detected
|
|
1299
|
-
? "MISSED"
|
|
1300
|
-
: !r.reasonOk
|
|
1301
|
-
? `WRONG REASON (expected ${r.expectedReason})`
|
|
1302
|
-
: "detected";
|
|
1319
|
+
const verdict = r.error
|
|
1320
|
+
? `${r.outcome}: ${r.error.split("\n")[0]}`
|
|
1321
|
+
: r.outcome;
|
|
1303
1322
|
console.log(` ${r.id}: ${verdict}`);
|
|
1304
1323
|
}
|
|
1305
1324
|
|
|
1306
1325
|
if (missed.length > 0) {
|
|
1307
1326
|
console.error(
|
|
1308
1327
|
`\n[PROOF_FAIL] ${CONTROL_SENSITIVITY ? "control_sensitivity_check" : "mutation_check"}: ${missed.length} of ${results.length} planted defect(s) were not properly detected. ` +
|
|
1309
|
-
`
|
|
1328
|
+
`See individual outcomes: unrelated failures and missing evidence do not demonstrate detection.`,
|
|
1310
1329
|
);
|
|
1311
1330
|
process.exit(1);
|
|
1312
1331
|
}
|