proof-harness 0.1.0-next.3 → 0.1.0-next.4

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 CHANGED
@@ -11,7 +11,7 @@ Consumers must pin an exact prerelease version, for example:
11
11
  ```json
12
12
  {
13
13
  "devDependencies": {
14
- "proof-harness": "0.1.0-next.3"
14
+ "proof-harness": "0.1.0-next.4"
15
15
  }
16
16
  }
17
17
  ```
@@ -24,3 +24,18 @@ tighten validation or packaging contracts even when its base version remains
24
24
  The trace, mission, and health protocol versions are independent from the npm
25
25
  package version. Consumers should reject unsupported artifact or protocol
26
26
  versions rather than guessing how to interpret them.
27
+
28
+ ## Prerelease migration notes
29
+
30
+ ### 0.1.0-next.4
31
+
32
+ No entry-point, artifact schema, or protocol changes. The `mutate` command
33
+ tightens validation: each mutation's subject is now read back after apply and
34
+ again after its proof, so a defect that never took effect fails as
35
+ `NOT PLANTED` and one that was externally undone mid-run fails as
36
+ `UN-PLANTED` — both distinct from a missed proof. A catalog mutation whose
37
+ `apply` intentionally leaves its subject unchanged (it plants a side object
38
+ that `cleanup` removes) must now declare `applyDoesNotChangeSubject: true`;
39
+ runs with such undeclared mutations fail until the flag is added. The shared
40
+ dev server is also started before the first defect is planted rather than
41
+ inside the first mutation's window.
@@ -941,6 +941,18 @@ operation }` assertion has neither a matching mutation nor a valid entry in
941
941
  `.proof/mutation-policy.json`, pinned to the current spec hash and a passing
942
942
  positive control. A changed/deleted claim, changed spec, missing control, or
943
943
  newly added mutation makes the acceptance stale and fails inventory.
944
+ - **A mutation that is not demonstrably live cannot judge a proof.** The
945
+ mutation runner never (re)applies migrations; it starts (or adopts) the
946
+ shared dev server before the first defect is planted, then reads each
947
+ mutation's subject back after apply and again after the proof. An apply that
948
+ left the subject unchanged fails as NOT PLANTED (a `REVOKE` no-op, or a
949
+ privilege still reachable via `PUBLIC` or an inherited role); a subject that
950
+ reverted mid-run — for example consumer tooling re-applying a wholesale
951
+ `REVOKE ALL; GRANT …` migration — fails as UN-PLANTED. Neither is reported
952
+ as a missed proof, because the proof never saw the defect. A mutation whose
953
+ apply intentionally leaves its subject untouched (it plants a side object
954
+ that `cleanup` removes) must declare `applyDoesNotChangeSubject: true` in
955
+ the catalog to opt out of the two subject comparisons explicitly.
944
956
 
945
957
  `pnpm proof:mutate` is the regression test for these guarantees: it re-opens known
946
958
  vulnerabilities in the local database one at a time and fails if the corresponding
package/README.md CHANGED
@@ -17,7 +17,7 @@ Public prereleases are published under the npm `next` tag. Pin an exact
17
17
  prerelease version while the API is stabilizing:
18
18
 
19
19
  ```sh
20
- pnpm add proof-harness@0.1.0-next.3
20
+ pnpm add proof-harness@0.1.0-next.4
21
21
  ```
22
22
 
23
23
  The package exposes environment-specific entry points:
@@ -12,7 +12,10 @@
12
12
  //
13
13
  // Mutations are applied to the DATABASE, not the source tree: they are precise,
14
14
  // instant and reversible, where patching SQL files would require a full
15
- // migration cycle per mutation.
15
+ // migration cycle per mutation. Migrations are never (re)applied by this
16
+ // script; each planted defect is read back after apply and again after the
17
+ // proof, so a plant that never took effect — or was externally undone
18
+ // mid-run — fails as its own finding instead of being blamed on the proof.
16
19
  //
17
20
  // The mutation list is also the honest answer to "what does the baseline suite
18
21
  // actually detect?" — each entry names a finding, the hole it re-opens, and the
@@ -39,6 +42,10 @@ import {
39
42
  evaluateMutationClaimCoverage,
40
43
  mutationCoversClaim,
41
44
  } from "./proof_mutation_inventory.mjs";
45
+ import {
46
+ assessPlantedSubject,
47
+ assessSubjectAfterProof,
48
+ } from "./proof_plant_verification.mjs";
42
49
  import {
43
50
  applySourceMutation,
44
51
  restoreSourceMutation,
@@ -913,6 +920,15 @@ Exits non-zero if any proof failed to notice its vulnerability.`);
913
920
  let devServer = null;
914
921
 
915
922
  try {
923
+ // Start (or adopt) the shared server BEFORE the first defect is planted.
924
+ // Consumer `pnpm dev` bootstrap hooks — migration re-application, seeding,
925
+ // schema reconciliation — must run against the healthy database, not
926
+ // inside the first mutation's window, where a wholesale re-GRANT would
927
+ // silently un-plant it.
928
+ if (selected.some((mutation) => !isSourceMutation(mutation))) {
929
+ devServer = await sharedDevServer();
930
+ }
931
+
916
932
  for (const m of selected) {
917
933
  console.log(`── ${m.id} ──────────────────────────────────────────`);
918
934
  console.log(` planting: ${m.breaks}`);
@@ -933,6 +949,19 @@ Exits non-zero if any proof failed to notice its vulnerability.`);
933
949
  applyMutation(container, m, snapshot);
934
950
  applied = true;
935
951
 
952
+ // A defect that is not demonstrably live proves nothing: running the
953
+ // proof anyway would blame the proof ("MISSED") for a plant that never
954
+ // happened. Read the subject back and refuse to continue on mismatch.
955
+ const plantedSnapshot = handler.snapshot(container, m.subject);
956
+ const plantProblem = assessPlantedSubject({
957
+ id: m.id,
958
+ description: handler.describe(m.subject),
959
+ before: snapshot,
960
+ afterApply: plantedSnapshot,
961
+ applyDoesNotChangeSubject: m.applyDoesNotChangeSubject === true,
962
+ });
963
+ if (plantProblem) throw new Error(plantProblem);
964
+
936
965
  if (isSourceMutation(m)) {
937
966
  if (devServer) {
938
967
  await devServer.stop();
@@ -947,6 +976,22 @@ Exits non-zero if any proof failed to notice its vulnerability.`);
947
976
  const { code, output } = await runProof(m.spec, m.id);
948
977
  const playwrightFailed = code !== 0;
949
978
 
979
+ // The defect must still be live now that the proof has finished. If
980
+ // the subject reverted mid-run (issue #7: a re-applied migration
981
+ // re-granting a revoked privilege), the proof's verdict — green or
982
+ // red — says nothing about the proof, and archiving it as MISSED
983
+ // would misdirect the operator toward a working proof.
984
+ const afterProofSnapshot = handler.snapshot(container, m.subject);
985
+ const liveProblem = assessSubjectAfterProof({
986
+ id: m.id,
987
+ description: handler.describe(m.subject),
988
+ before: snapshot,
989
+ afterApply: plantedSnapshot,
990
+ afterProof: afterProofSnapshot,
991
+ applyDoesNotChangeSubject: m.applyDoesNotChangeSubject === true,
992
+ });
993
+ if (liveProblem) throw new Error(liveProblem);
994
+
950
995
  // A proof can fail for the wrong reason (a crash, a timeout, a missing
951
996
  // browser). Where the reason matters — as with the deny-all mutation that
952
997
  // only a positive control can catch — require it in the output.
@@ -0,0 +1,88 @@
1
+ // ---------------------------------------------------------------------------
2
+ // proof_plant_verification.mjs
3
+ //
4
+ // A mutation run is only evidence if the planted defect was actually live for
5
+ // the entire proof. These checks compare the subject's snapshot at three
6
+ // moments — before apply, after apply, and after the proof — and turn every
7
+ // silent mismatch into a structured, correctly-attributed failure instead of
8
+ // letting it masquerade as "MISSED — the proof cannot fail":
9
+ //
10
+ // - apply changed nothing → the defect never went live (NOT PLANTED)
11
+ // - subject reverted mid-proof → something un-planted it (UN-PLANTED)
12
+ // - subject drifted mid-proof → the run cannot be attributed
13
+ //
14
+ // Without them, an ineffective REVOKE (the privilege still reachable via
15
+ // PUBLIC or an inherited role, a grantor no-op, or tooling that re-applies
16
+ // migrations mid-run) leaves the proof green against a healthy database, and
17
+ // the summary blames the proof for a defect that was never live.
18
+ //
19
+ // A mutation whose apply intentionally leaves its subject untouched — it
20
+ // plants a side object that `cleanup` removes, and the subject only anchors
21
+ // restore — must declare `applyDoesNotChangeSubject: true` in the catalog.
22
+ // Such mutations keep the historical blind spot by explicit, reviewable
23
+ // choice rather than by accident.
24
+ // ---------------------------------------------------------------------------
25
+
26
+ /**
27
+ * Judge the subject immediately after `apply`. Returns an error message when
28
+ * the run must stop, or null when the plant is coherent.
29
+ */
30
+ export function assessPlantedSubject({
31
+ id,
32
+ description,
33
+ before,
34
+ afterApply,
35
+ applyDoesNotChangeSubject = false,
36
+ }) {
37
+ if (applyDoesNotChangeSubject) {
38
+ if (afterApply === before) return null;
39
+ return (
40
+ `mutation ${id} declares applyDoesNotChangeSubject, but applying it changed ` +
41
+ `${description}. The declaration is wrong; remove it so the subject is verified.\n` +
42
+ ` before apply: ${before}\n after apply: ${afterApply}`
43
+ );
44
+ }
45
+ if (afterApply !== before) return null;
46
+ return (
47
+ `mutation ${id} is NOT PLANTED: ${description} reads exactly the same before and after ` +
48
+ `apply, so the defect is not live and its proof would run against a healthy database. ` +
49
+ `Common causes: the privilege is still held via PUBLIC or an inherited role, the ` +
50
+ `GRANT/REVOKE was a silent no-op, or concurrent tooling immediately restored the subject. ` +
51
+ `If the apply intentionally leaves the subject untouched (it plants a side object that ` +
52
+ `cleanup removes), declare applyDoesNotChangeSubject: true on the mutation.\n` +
53
+ ` subject snapshot: ${before}`
54
+ );
55
+ }
56
+
57
+ /**
58
+ * Judge the subject after the proof ran, before restore. Returns an error
59
+ * message when the defect was no longer (or differently) planted, or null
60
+ * when the proof demonstrably ran against the planted defect.
61
+ */
62
+ export function assessSubjectAfterProof({
63
+ id,
64
+ description,
65
+ before,
66
+ afterApply,
67
+ afterProof,
68
+ applyDoesNotChangeSubject = false,
69
+ }) {
70
+ const expected = applyDoesNotChangeSubject ? before : afterApply;
71
+ if (afterProof === expected) return null;
72
+ if (afterProof === before) {
73
+ return (
74
+ `mutation ${id} was UN-PLANTED while its proof ran: ${description} returned to its ` +
75
+ `pre-mutation state before the proof finished, so the proof never had a chance to detect ` +
76
+ `the defect and its verdict is meaningless. Something outside the harness restored the ` +
77
+ `subject mid-run — typically dev-server bootstrap or migration tooling re-applying grants ` +
78
+ `(for example a wholesale "REVOKE ALL; GRANT ..." migration). This is a ` +
79
+ `harness/environment failure, not a missed proof.`
80
+ );
81
+ }
82
+ return (
83
+ `mutation ${id} lost its planted state while its proof ran: ${description} changed to a ` +
84
+ `state matching neither the planted defect nor the original, so the run cannot be ` +
85
+ `attributed to the mutation.\n` +
86
+ ` before apply: ${before}\n after apply: ${afterApply}\n after proof: ${afterProof}`
87
+ );
88
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "proof-harness",
3
- "version": "0.1.0-next.3",
3
+ "version": "0.1.0-next.4",
4
4
  "description": "Verification and conformance helpers for proof traces, missions, fixtures, and Playwright probes.",
5
5
  "license": "Apache-2.0",
6
6
  "author": {