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 +106 -4
- package/README.md +87 -2
- 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.d.ts +4 -0
- package/dist/playwright.js +110 -50
- package/dist/playwright.js.map +1 -1
- package/package.json +1 -1
- package/proof-consumer-fixtures/parallel/contract.json +46 -0
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.
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
+
}
|