proof-harness 0.1.0-next.12 → 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 CHANGED
@@ -27,10 +27,112 @@ versions rather than guessing how to interpret them.
27
27
 
28
28
  ## Prerelease migration notes
29
29
 
30
- ### 0.1.0-next.12 — independent isolation fixtures and column ACL recovery (candidate)
31
-
32
- Not yet published. Keep the existing consumer pin until this release is reviewed
33
- and published; then pin exactly `proof-harness@0.1.0-next.12`.
30
+ ### 0.1.0-next.14 — isolated parallel proof gate (release candidate)
31
+
32
+ Prepared for review, not yet published. Existing serial commands and assertion
33
+ APIs remain supported. After a reviewed publication, consumers adopting the
34
+ parallel gate must pin exactly `proof-harness@0.1.0-next.14` and update their
35
+ lockfile. Do not switch the required release check before parity validation.
36
+
37
+ The optional `plan`, `verify --plan`, `mutate --plan`, `controls --plan`,
38
+ `finalize`, and `aggregate` commands divide complete proof specs among isolated
39
+ checkouts. No product claims, fixtures, policies or mutation catalogs move into
40
+ the package. Trace schema 2, mission schema 1 and health protocol 1 are unchanged.
41
+ Parallel plans, stage records, execution reports and acceptance reports have
42
+ separate version-1 contracts. They require the exact same harness version,
43
+ source contents, commit, lockfile, generated schema/capabilities, configs,
44
+ catalogs, policies and CI run/attempt throughout a run.
45
+
46
+ Required integration for parallel execution:
47
+
48
+ 1. Add `.proof/parallel/` to the consumer's generated-output ignores (or configure
49
+ `artifacts.parallel`). Use fresh checkouts and fresh output directories for
50
+ each run. Downloaded artifacts also belong under this excluded directory.
51
+ 2. Declare `parallel.postChecks` in the protected consumer config as a nonempty
52
+ array of command argv arrays. Include the consumer's existing database
53
+ permission and browser permission controls. The package executes every
54
+ declared command in `finalize`, retains failures, and requires the checks at
55
+ aggregation. These are consumer trust inputs; the package invents none.
56
+ 3. Use one shared planning job, isolated shard jobs, and an always-run final
57
+ aggregation job. Each shard owns its checkout, local database, dev server,
58
+ fake services, browser state, build outputs and recovery journal. Two shards
59
+ cannot share one checkout. Workers remain one within each shard, and
60
+ mutation retries remain zero. An active-stage lock prevents concurrent stage
61
+ execution within a checkout.
62
+ 4. Regenerate schema/capabilities and perform the existing structural preflight
63
+ on the same source in each job. Generated artifacts must be deterministic.
64
+ Keep the existing database baseline and pre-mutation permission checks.
65
+ The planner uses Playwright discovery, including specs with no mutations;
66
+ a scoped baseline runs only the assigned complete specs. Automatic mutations
67
+ are derived from that fresh evidence and rederived from combined evidence
68
+ during aggregation.
69
+ 5. Run `finalize` and upload that shard's directory even when a preceding
70
+ mutation/control command fails. Keep matrix `fail-fast: false`. A missing,
71
+ cancelled or failed stage cannot satisfy the final gate. Preserve the
72
+ existing required check name on the final aggregation job.
73
+ 6. Compare serial and parallel runs of the same commit on separate environments:
74
+ complete test/claim/mutation/control inventories, coverage, exceptions and
75
+ post-run checks must agree. Validate the packed package and run the
76
+ consumer's full validation. Measure at least three complete green CI runs
77
+ before making the parallel gate required. Include setup, artifact transfer,
78
+ planning and aggregation in the runtime target; package timings omit those
79
+ external costs. Retain a serial command for investigation and comparison.
80
+
81
+ Every discovered baseline test must pass and emit trace evidence with matching
82
+ Playwright execution identity. Skipped tests and expected failures cannot count
83
+ as a complete baseline. Baseline retries retain their configured behavior and
84
+ all attempts appear in execution reports. Playwright project dependencies are
85
+ currently rejected by planning: provision shared prerequisites explicitly in
86
+ consumer setup instead. Proof specs must not depend on another spec's fixtures
87
+ or order. Mutation runs must produce complete, non-timeout execution reports as
88
+ well as the correctly mapped failing assertions.
89
+
90
+ `verify --spec <path> --traces-dir <fresh-directory>` and
91
+ `mutate|controls --spec <path> --baseline-dir <directory>` also support focused
92
+ local diagnosis; repeat `--spec` for multiple specs. They validate the selected
93
+ baseline independently of unrelated old traces. Partial success is not full
94
+ release acceptance. Full coverage and mission validation occur only after the
95
+ parallel aggregator has validated every shard. A custom parallel output path
96
+ must stay excluded from source identity through `artifacts.parallel`.
97
+
98
+ Mutation summaries retain schema version 1 and add `outcome`, `restoration`,
99
+ `cleanupErrors`, execution status and phase timings. Each mutation produces one
100
+ lifecycle result. An unrelated red claim, missing evidence, timeout or failed
101
+ plant is not reported as a proof that cannot fail. Restore failures retain the
102
+ recovery journal and block acceptance independently of detection. Original
103
+ execution errors and later cleanup errors remain separately available.
104
+
105
+ Timing history is optional and influences balancing only. It cannot remove
106
+ work or satisfy evidence. There is no evidence reuse or partial-job resume in
107
+ this release: a rerun needs a new plan and all shard results from that attempt.
108
+ After an interrupted local run, use the existing `mutate --recover` procedure
109
+ before discarding its checkout or output; a successful recovery is not a passing
110
+ parallel run. The next clean run must establish new evidence.
111
+
112
+
113
+ ### 0.1.0-next.13 — column-restricted tenant isolation (release candidate)
114
+
115
+ Prepared for release. After publication, pin exactly `proof-harness@0.1.0-next.13`
116
+ and update the lockfile. Add `columns` only where column grants require it, then
117
+ rerun the baseline, coverage, mutation and control checks against the real database.
118
+ Existing consumers that omit this option need no API changes.
119
+
120
+ `assert.tenantIsolation` accepts optional `columns: ["id", "account_id"]` for
121
+ private tables that grant customers only selected columns. The SDK uses the
122
+ same projection for service ground truth, owner controls, and outsider probes.
123
+ Only distinct plain column names are allowed; empty lists, wildcards, aliases,
124
+ aggregates, casts and joins are rejected before fixture setup. Omitting the
125
+ option retains the existing all-column query.
126
+
127
+ This proves row isolation for the selected columns. Consumers must separately
128
+ prove that private columns remain unreadable; do not grant extra columns to
129
+ make a fixture pass. Existing positive-control, bidirectional, non-vacuity and
130
+ provenance requirements are unchanged. No trace, mission, or health protocol migration is
131
+ required. Adopt only an exact reviewed published package version.
132
+
133
+ ### 0.1.0-next.12 — independent isolation fixtures and column ACL recovery
134
+
135
+ Published release. Pin exactly `proof-harness@0.1.0-next.12` when adopting these changes.
34
136
 
35
137
  `assert.tenantIsolation` now accepts a caller-owned `isolationFixture` instead of
36
138
  workspace seeding. Supply a stable fixture `id`, the matching `table`, an explicit
package/README.md CHANGED
@@ -107,6 +107,87 @@ Disposable users that authenticate a Playwright page must call
107
107
  `assert.tenantIsolation({ page })` performs that browser cleanup automatically;
108
108
  the page is logged out when the helper returns.
109
109
 
110
+ ## Optional isolated parallel execution
111
+
112
+ The next.14 release candidate adds spec-based sharding without changing the
113
+ serial workflow. Adopt it only after review and publication; see the
114
+ [required integration and parity checks](COMPATIBILITY.md#010-next14--isolated-parallel-proof-gate-release-candidate).
115
+
116
+ The consumer supplies its own post-run commands in `proof.config.mjs`. For
117
+ example, if these scripts exist and cover its database and browser controls:
118
+
119
+ ```js
120
+ export default {
121
+ // Existing paths, catalogs and trust inputs remain here.
122
+ parallel: {
123
+ postChecks: [
124
+ ["pnpm", "test:permissions"],
125
+ ["pnpm", "test:profile-permissions"],
126
+ ],
127
+ },
128
+ };
129
+ ```
130
+
131
+ After the existing environment setup and structural preflight, create a plan:
132
+
133
+ ```sh
134
+ pnpm exec proof-harness plan --shards 6 \
135
+ --controls-config proof.controls.config.mjs \
136
+ --out .proof/parallel/plan.json
137
+ ```
138
+
139
+ Upload the plan for the shard jobs. Each gets a separate checkout, local
140
+ Supabase stack and fake services. After its normal setup and preflight,
141
+ download the plan and run these commands with its zero-based matrix index:
142
+
143
+ ```sh
144
+ pnpm exec proof-harness verify --plan .proof/parallel/plan.json --shard 0
145
+ pnpm exec proof-harness mutate --plan .proof/parallel/plan.json --shard 0
146
+ pnpm exec proof-harness controls --plan .proof/parallel/plan.json --shard 0
147
+ pnpm exec proof-harness finalize --plan .proof/parallel/plan.json --shard 0
148
+ ```
149
+
150
+ Make `finalize` an `if: always()` step, followed by an always-run artifact upload
151
+ of `.proof/parallel/shard-0/` including hidden files. Store each shard under its
152
+ own name and download them as `inputs/shard-0`, `inputs/shard-1`, etc. under
153
+ `.proof/parallel/`. Upload only the plan from the planning job; never distribute
154
+ `environment.json` or `active-stage.lock` between runners. Use six matrix values
155
+ `[0, 1, 2, 3, 4, 5]` with `fail-fast: false`; use fewer for a smaller repository.
156
+ The final job downloads all shards and the plan, regenerates discovery inputs,
157
+ and runs:
158
+
159
+ ```sh
160
+ pnpm exec proof-harness aggregate --plan .proof/parallel/plan.json \
161
+ --inputs .proof/parallel/inputs
162
+ ```
163
+
164
+ The final job must run even when a shard fails, and its status must also require
165
+ successful planning and shard jobs. Only this complete gate replaces the
166
+ consumer's existing required Proof check. Keep consumer structural, database
167
+ baseline and pre-mutation permission checks in the workflow; `aggregate`
168
+ validates the complete baseline/mission, strict coverage, independently derived
169
+ mutation inventories, mapped failures, restoration and configured postchecks.
170
+
171
+ GitHub's run ID and attempt identify the run automatically. For local tests,
172
+ set a unique `PROOF_RUN_ID` in every checkout for the same run. Local shards need
173
+ distinct servers and databases as well as checkouts. Do not run multiple shards
174
+ against one mutable environment, even if their traces use different folders.
175
+
176
+ Successful aggregation writes an acceptance report with stage durations, slow
177
+ specs and total runner time under `.proof/parallel/aggregate-*/acceptance.json`.
178
+ Pass a previous report with `plan --history <file>` to balance by measured spec
179
+ costs. History changes scheduling only; current source and catalogs determine
180
+ all required work. Mutation execution reports retain attempts and first errors;
181
+ SDK fixture setup/cleanup steps provide additional diagnostics. Startup and
182
+ compilation remain part of readiness/execution where no separate measurement
183
+ exists. An 18-minute lane warning signals that CI overhead may push the overall
184
+ Proof job beyond a 20-minute budget.
185
+
186
+ This is additional parallel capacity, not a fixed runtime guarantee. Benchmark
187
+ with the consumer before switching its required gate, and compare total billed
188
+ runner-minutes as well as elapsed time. Keep caches limited to dependencies;
189
+ do not reuse evidence, database state or mutable application builds.
190
+
110
191
  ## License
111
192
 
112
193
  Copyright 2026 The Nexus Collective GmbH.
@@ -157,6 +238,10 @@ privileged writes and requires action-boundary evidence even without workspace
157
238
  inputs or for internal actions. See [required migration steps](COMPATIBILITY.md)
158
239
  before adopting the exact version.
159
240
 
160
- Release candidate `0.1.0-next.12` adds caller-owned account/user isolation fixtures
241
+ Release `0.1.0-next.12` adds caller-owned account/user isolation fixtures
161
242
  and restores column grants and grant options after table-privilege mutations.
162
- Keep existing pins until publication; see [migration requirements](COMPATIBILITY.md).
243
+ See [migration requirements](COMPATIBILITY.md).
244
+
245
+ Release candidate `0.1.0-next.13` adds optional readable-column selection to
246
+ `assert.tenantIsolation` for tables with column grants. Private-column denial
247
+ remains a separate consumer proof; see [migration requirements](COMPATIBILITY.md).
package/cli/config.mjs CHANGED
@@ -10,6 +10,7 @@ const DEFAULT_CONFIG = Object.freeze({
10
10
  drift: ".proof/drift.json",
11
11
  mutations: ".proof/mutations",
12
12
  controlSensitivity: ".proof/control-sensitivity",
13
+ parallel: ".proof/parallel",
13
14
  },
14
15
  // The module-descriptor system (module.meta.ts → modules.json and its
15
16
  // checker) moved to the consumer template in 0.1.0-next.6: it serves the
@@ -65,7 +65,7 @@ process.chdir(CONFIG.rootDir);
65
65
 
66
66
  const SCHEMA_PATH = CONFIG.artifacts.schema;
67
67
  const CAPABILITIES_PATH = CONFIG.artifacts.capabilities;
68
- const TRACES_DIR = CONFIG.artifacts.traces;
68
+ let TRACES_DIR = CONFIG.artifacts.traces;
69
69
  const POLICY_PATH = CONFIG.policies.coverage;
70
70
 
71
71
  /**
@@ -744,6 +744,13 @@ function printReport(report) {
744
744
 
745
745
  export function main() {
746
746
  const args = process.argv.slice(2);
747
+ const directoryIndex = args.indexOf("--traces-dir");
748
+ if (directoryIndex >= 0) {
749
+ if (!args[directoryIndex + 1] || args[directoryIndex + 1].startsWith("--"))
750
+ throw new Error("--traces-dir requires a directory");
751
+ TRACES_DIR = args[directoryIndex + 1];
752
+ args.splice(directoryIndex, 2);
753
+ }
747
754
  const strict = args.includes("--strict");
748
755
  const asJson = args.includes("--json");
749
756
 
@@ -0,0 +1,65 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+
4
+ /** Sidecar only: trace, mission and health contracts remain unchanged. */
5
+ export default class ProofExecutionReporter {
6
+ onBegin(_config, suite) {
7
+ this.started = performance.now();
8
+ this.report = {
9
+ schemaVersion: 1,
10
+ token: process.env.PROOF_EXECUTION_TOKEN ?? null,
11
+ tests: suite.allTests().map((test) => ({
12
+ id: `${test.parent.project().name}:${test.id}:${test.repeatEachIndex}`,
13
+ spec: path
14
+ .relative(process.cwd(), test.location.file)
15
+ .split(path.sep)
16
+ .join("/"),
17
+ title: test.titlePath(),
18
+ expectedStatus: test.expectedStatus,
19
+ attempts: [],
20
+ fixtureSpans: [],
21
+ })),
22
+ errors: [],
23
+ };
24
+ }
25
+ onStepEnd(test, result, step) {
26
+ if (!["proof fixture setup", "proof fixture cleanup"].includes(step.title))
27
+ return;
28
+ const id = `${test.parent.project().name}:${test.id}:${test.repeatEachIndex}`;
29
+ this.report.tests
30
+ .find((t) => t.id === id)
31
+ .fixtureSpans.push({
32
+ phase: step.title,
33
+ retry: result.retry,
34
+ durationMs: step.duration,
35
+ failed: Boolean(step.error),
36
+ });
37
+ }
38
+ onTestEnd(test, result) {
39
+ const id = `${test.parent.project().name}:${test.id}:${test.repeatEachIndex}`;
40
+ this.report.tests
41
+ .find((t) => t.id === id)
42
+ .attempts.push({
43
+ retry: result.retry,
44
+ status: result.status,
45
+ durationMs: result.duration,
46
+ errors: result.errors.map((e) => e.message ?? String(e)),
47
+ });
48
+ }
49
+ onError(error) {
50
+ this.report?.errors.push(error.message ?? String(error));
51
+ }
52
+ onEnd(result) {
53
+ if (!this.report || !process.env.PROOF_EXECUTION_REPORT) return;
54
+ this.report.status = result.status;
55
+ this.report.durationMs = performance.now() - this.started;
56
+ fs.mkdirSync(path.dirname(process.env.PROOF_EXECUTION_REPORT), {
57
+ recursive: true,
58
+ });
59
+ fs.writeFileSync(
60
+ process.env.PROOF_EXECUTION_REPORT,
61
+ JSON.stringify(this.report, null, 2) + "\n",
62
+ { flag: "wx" },
63
+ );
64
+ }
65
+ }
@@ -0,0 +1,136 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { isArtifactId, readTraceDirectory } from "../../dist/node.js";
4
+ import { evidenceExclusions } from "../config.mjs";
5
+ import { validateControlBaseline } from "./proof_control_sensitivity.mjs";
6
+ import {
7
+ deriveAutomaticRlsMutations,
8
+ evaluateMutationClaimCoverage,
9
+ mutationCoversClaim,
10
+ } from "./proof_mutation_inventory.mjs";
11
+
12
+ function readMutationPolicy(MUTATION_POLICY_PATH) {
13
+ try {
14
+ const policy = JSON.parse(fs.readFileSync(MUTATION_POLICY_PATH, "utf8"));
15
+ if (policy.schemaVersion !== 1) {
16
+ return {
17
+ policy: { acceptedClaims: [] },
18
+ problems: [
19
+ `${MUTATION_POLICY_PATH} has unsupported schemaVersion ${policy.schemaVersion ?? "<missing>"}; expected 1`,
20
+ ],
21
+ };
22
+ }
23
+ return { policy, problems: [] };
24
+ } catch (error) {
25
+ return {
26
+ policy: { acceptedClaims: [] },
27
+ problems: [
28
+ `cannot read ${MUTATION_POLICY_PATH}: ${error instanceof Error ? error.message : String(error)}`,
29
+ ],
30
+ };
31
+ }
32
+ }
33
+
34
+ export function buildMutationInventory({
35
+ config: CONFIG,
36
+ mutations: catalog,
37
+ controlSensitivity: CONTROL_SENSITIVITY = false,
38
+ tracesDir: TRACE_DIR = CONFIG.artifacts.traces,
39
+ specs = /** @type {string[] | null} */ (null),
40
+ }) {
41
+ if (!catalog.every((m) => isArtifactId(m.id) && typeof m.spec === "string"))
42
+ throw new Error("invalid mutation identity");
43
+ const MUTATIONS = specs
44
+ ? catalog.filter((m) => specs.includes(m.spec))
45
+ : catalog;
46
+ TRACE_DIR = path.resolve(CONFIG.rootDir, TRACE_DIR);
47
+ const MUTATION_POLICY_PATH = path.resolve(
48
+ CONFIG.rootDir,
49
+ CONFIG.policies.mutation,
50
+ );
51
+ const SCHEMA_PATH = path.resolve(CONFIG.rootDir, CONFIG.artifacts.schema);
52
+ if (CONTROL_SENSITIVITY) {
53
+ if (!CONFIG.controlSensitivityCatalog)
54
+ throw new Error(
55
+ "[PROOF_FAIL] control_contract: configure a separate controlSensitivityCatalog",
56
+ );
57
+ const traces = readTraceDirectory(TRACE_DIR, {
58
+ rootDir: CONFIG.rootDir,
59
+ excludedPaths: evidenceExclusions(CONFIG),
60
+ });
61
+ const ids = new Set();
62
+ const mutations = MUTATIONS.map((m) => {
63
+ if (ids.has(m.id)) throw new Error("duplicate control-sensitivity id");
64
+ ids.add(m.id);
65
+ return { ...m, resolvedControls: validateControlBaseline(m, traces) };
66
+ });
67
+ return {
68
+ mutations,
69
+ problems: [],
70
+ claims: [],
71
+ acceptedClaims: [],
72
+ uncoveredClaims: [],
73
+ uncoveredActionClaims: [],
74
+ };
75
+ }
76
+ const derived = deriveAutomaticRlsMutations({
77
+ tracesDir: TRACE_DIR,
78
+ rootDir: CONFIG.rootDir,
79
+ excludedPaths: evidenceExclusions(CONFIG),
80
+ schemaPath: SCHEMA_PATH,
81
+ explicitMutations: MUTATIONS,
82
+ });
83
+ const mutations = [...MUTATIONS, ...derived.mutations];
84
+ const ids = new Set();
85
+ const problems = [...derived.problems];
86
+
87
+ for (const mutation of mutations) {
88
+ if (ids.has(mutation.id)) {
89
+ problems.push(`duplicate mutation id "${mutation.id}"`);
90
+ }
91
+ ids.add(mutation.id);
92
+ }
93
+
94
+ const loadedPolicy = readMutationPolicy(MUTATION_POLICY_PATH);
95
+ if (specs)
96
+ loadedPolicy.policy = {
97
+ ...loadedPolicy.policy,
98
+ acceptedClaims: (loadedPolicy.policy.acceptedClaims ?? []).filter((c) =>
99
+ specs.includes(c.spec),
100
+ ),
101
+ };
102
+ const claimCoverage = evaluateMutationClaimCoverage({
103
+ claims: derived.claims,
104
+ evidence: derived.evidence,
105
+ mutations,
106
+ policy: loadedPolicy.policy,
107
+ });
108
+ problems.push(...loadedPolicy.problems, ...claimCoverage.problems);
109
+ for (const claim of claimCoverage.uncoveredClaims) {
110
+ problems.push(
111
+ `${claim.spec} has no mutation or accepted policy entry for ${claim.kind}:${claim.target}:${claim.operation}`,
112
+ );
113
+ }
114
+
115
+ const mutationsWithClaims = mutations.map((mutation) => ({
116
+ ...mutation,
117
+ resolvedClaims: derived.claims
118
+ .filter((claim) => mutationCoversClaim(mutation, claim))
119
+ .map((claim) => ({
120
+ kind: claim.kind,
121
+ target: claim.target,
122
+ operation: claim.operation,
123
+ })),
124
+ }));
125
+
126
+ return {
127
+ mutations: mutationsWithClaims,
128
+ problems,
129
+ claims: derived.claims,
130
+ acceptedClaims: claimCoverage.acceptedClaims,
131
+ uncoveredClaims: claimCoverage.uncoveredClaims,
132
+ uncoveredActionClaims: claimCoverage.uncoveredClaims.filter((claim) =>
133
+ /^[A-Za-z][\w-]*:[A-Za-z][\w-]*$/.test(claim.target),
134
+ ),
135
+ };
136
+ }