proof-harness 0.1.0-next.5 → 0.1.0-next.7
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 +95 -1
- package/PROOF_SDK_CONTRACT.md +22 -88
- package/README.md +19 -5
- package/cli/config.mjs +34 -15
- package/cli/engines/parse_proof_schema.mjs +40 -6
- package/cli/engines/proof_coverage.mjs +11 -17
- package/cli/engines/proof_drift.mjs +105 -93
- package/cli/engines/proof_mutation_check.mjs +189 -56
- package/cli/engines/proof_mutation_inventory.mjs +16 -21
- package/cli/engines/proof_verify.mjs +56 -37
- package/cli/engines/scan_proof_capabilities.mjs +309 -204
- package/cli/proof-harness.mjs +1 -5
- package/dist/{mission-types-Dr5rBHYO.d.ts → mission-types-CYXrtv5m.d.ts} +3 -3
- package/dist/node.d.ts +31 -3
- package/dist/node.js +320 -47
- package/dist/node.js.map +1 -1
- package/dist/playwright.d.ts +3 -1
- package/dist/playwright.js +577 -229
- package/dist/playwright.js.map +1 -1
- package/dist/server.js +42 -29
- package/dist/server.js.map +1 -1
- package/dist/shared.d.ts +2 -167
- package/dist/shared.js +0 -32
- package/dist/shared.js.map +1 -1
- package/dist/{trace-types-DkIzq-l9.d.ts → trace-types-BIW53YPH.d.ts} +6 -22
- package/package.json +3 -3
- package/proof-consumer-fixtures/malformed/contradictory-status.json +45 -0
- package/cli/engines/proof_module_check.mjs +0 -653
- package/cli/engines/scan_module_meta.mjs +0 -352
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.
|
|
14
|
+
"proof-harness": "0.1.0-next.6"
|
|
15
15
|
}
|
|
16
16
|
}
|
|
17
17
|
```
|
|
@@ -27,6 +27,100 @@ versions rather than guessing how to interpret them.
|
|
|
27
27
|
|
|
28
28
|
## Prerelease migration notes
|
|
29
29
|
|
|
30
|
+
### Unreleased audit hardening
|
|
31
|
+
|
|
32
|
+
Release candidate `0.1.0-next.7` tightens acceptance behavior; it has not been published. Trace
|
|
33
|
+
schema remains 2, with additive source/execution identity fields. Version 1
|
|
34
|
+
and unversioned trace shapes remain explicitly readable, but CLI acceptance
|
|
35
|
+
requires current provenance. Required consumer changes:
|
|
36
|
+
|
|
37
|
+
- Regenerate traces. `verify`, `coverage`, and mutation inventory share a
|
|
38
|
+
decoder: corrupt JSON, duplicate proof IDs, unknown vocabulary, and
|
|
39
|
+
contradictory verdicts fail. Only passing baseline evidence can satisfy
|
|
40
|
+
claims; mutation evidence is separate. Current spec contents must match
|
|
41
|
+
`specHash`; in Git worktrees both `commit` and `sourceHash` must match.
|
|
42
|
+
`sourceHash` includes tracked and untracked nonignored inputs and excludes
|
|
43
|
+
configured generated artifacts. Run generators before proofs. Non-Git
|
|
44
|
+
exports cannot establish whole-source freshness and only check spec hashes.
|
|
45
|
+
- Use a dedicated trace directory. The writer honors `artifacts.traces` and
|
|
46
|
+
the runner's `--traces-dir` override. IDs must use letters, digits, dots,
|
|
47
|
+
underscores, or hyphens, starting with a letter or digit. Duplicate IDs
|
|
48
|
+
cannot overwrite another proof; a later Playwright retry of the same test
|
|
49
|
+
may replace its earlier attempt. Standalone repeated runs need fresh output.
|
|
50
|
+
Mission aggregates now live at `<traces>/missions/<missionId>.json` to
|
|
51
|
+
prevent collisions with proof IDs. Cleanup refuses unknown JSON before
|
|
52
|
+
deleting any trace and rejects repository/ancestor directories, including
|
|
53
|
+
symlink aliases. Move legacy or corrupt files aside before rerunning.
|
|
54
|
+
- UPDATE denial probes require stable `identityColumns` (default `["id"]`),
|
|
55
|
+
present and non-null in every target row, excluded from the update payload.
|
|
56
|
+
The attempted update must change a value. Rereads use original identities,
|
|
57
|
+
even when a write changes a filter column. SQL errors other than `42501`
|
|
58
|
+
are incomplete evidence. INSERT probes require a payload absent before the
|
|
59
|
+
probe and valid for a service-role control insertion; that control is
|
|
60
|
+
cleaned up. HTTP expectations must contain a status or a nonempty matcher
|
|
61
|
+
list. Write requests are never automatically retried after transport errors.
|
|
62
|
+
- Capability discovery uses TypeScript syntax analysis for import aliases and
|
|
63
|
+
multiple actions. Unknown factory/client indirection, escaped service query
|
|
64
|
+
builders, dynamic table names, and ambiguous metadata block assessment.
|
|
65
|
+
Discovery still assumes the configured template action layout.
|
|
66
|
+
- The SQL parser is deliberately conservative, not a final PostgreSQL catalog
|
|
67
|
+
evaluator. Unsupported ALTER/DROP or dynamic SQL makes schema assessment
|
|
68
|
+
fail. RLS must be explicitly enabled for classification; public ALL/write
|
|
69
|
+
policies are never exempted as public-read. Policy expressions and mode
|
|
70
|
+
participate in drift. Resolve unsupported migrations with a catalog-backed
|
|
71
|
+
integration before using this parser as a gate; do not edit shipped SQL or
|
|
72
|
+
mark an unassessed artifact assessed to obtain a pass.
|
|
73
|
+
- Drift always regenerates both trees using the installed harness and each
|
|
74
|
+
tree's consumer configuration. It includes untracked actions, SQL, and
|
|
75
|
+
lockfiles. For ignored/generated migrations, configure `driftPrepare` as a
|
|
76
|
+
command argv array (for example `["node", "scripts/aggregate_migrations.mjs",
|
|
77
|
+
"--fresh"]`). That consumer-owned command runs in each tree before parsing;
|
|
78
|
+
failure prevents assessment. It must work in the archived tree without an
|
|
79
|
+
implicit dependency install. The old `driftSources` optimization is unused.
|
|
80
|
+
- Mutations require fresh mapped baseline claims and write isolated traces.
|
|
81
|
+
SQL applies run as one transaction; recovery is attempted even after a
|
|
82
|
+
failed apply. Policy snapshots preserve expressions, roles, command, and
|
|
83
|
+
permissive/restrictive mode. Generic privilege mutations support direct,
|
|
84
|
+
owner-issued grants without grant options; unsupported inherited/PUBLIC or
|
|
85
|
+
ownership cases fail before planting. Move old mutation outputs aside on
|
|
86
|
+
first use: nonempty output directories require a harness ownership marker.
|
|
87
|
+
An interrupted run leaves `mutation-recovery.json` beside the mutation output
|
|
88
|
+
directory. Run `proof-harness mutate --recover` against the same local
|
|
89
|
+
database to restore and verify the journaled subject. A timeout, signal, or
|
|
90
|
+
incomplete failed claim never counts as detection. Recovery covers the
|
|
91
|
+
catalog's declared subject and cleanup, not arbitrary undeclared side effects.
|
|
92
|
+
- Partial fixture setup is cleaned up, and cleanup errors now fail the proof.
|
|
93
|
+
|
|
94
|
+
These checks validate observed evidence. Protect the installed harness,
|
|
95
|
+
configuration, mission, policies, mutation catalog, and CI acceptance job from
|
|
96
|
+
executor edits. Trace metadata is not a signature or a sandbox: it cannot
|
|
97
|
+
establish honesty against an executor allowed to replace the gate or fabricate
|
|
98
|
+
its inputs. Independent product acceptance and a trusted runner remain needed.
|
|
99
|
+
|
|
100
|
+
### 0.1.0-next.6
|
|
101
|
+
|
|
102
|
+
**Removed: the module-descriptor system.** It served the consumer's planner
|
|
103
|
+
(what a module is for, what a buyer must decide, where answers land) and never
|
|
104
|
+
fed verification, so it is now consumer-owned. Gone from the package:
|
|
105
|
+
|
|
106
|
+
- CLI commands `modules` and `modules-check` (and the `modules` step inside
|
|
107
|
+
`build`); the `.proof/module-policy.json` accept-list introduced in
|
|
108
|
+
`next.5` went with the checker.
|
|
109
|
+
- `proof-harness/shared` exports `ModuleMeta`, `ModuleDecision`,
|
|
110
|
+
`ModuleInput`, `ModuleDefault`, `ModuleSeam`, `ScannedModule`,
|
|
111
|
+
`ModulesArtifact`, `MODULE_DESCRIPTOR_SCHEMA_VERSION`, `MODULE_KINDS`,
|
|
112
|
+
`APPLIED_BY`, `ModuleKind`, `AppliedBy`, `isModuleKind`, `isAppliedBy`.
|
|
113
|
+
- The `proof.config.mjs` keys `artifacts.modules`, `policies.module`,
|
|
114
|
+
`roots.modules`, `roots.sharedModules`, and `moduleKinds` (now ignored if
|
|
115
|
+
supplied).
|
|
116
|
+
|
|
117
|
+
Migration: consumers using the system vendor it from
|
|
118
|
+
`proof-harness@0.1.0-next.5` (Apache-2.0) — `cli/engines/scan_module_meta.mjs`,
|
|
119
|
+
`cli/engines/proof_module_check.mjs`, and `src/shared/module-types.ts` plus the
|
|
120
|
+
two vocabularies above — and point their `module.meta.ts` type imports at the
|
|
121
|
+
vendored copy. Consumers that never ran `proof:modules` are unaffected. Trace,
|
|
122
|
+
mission, and health protocol versions are unchanged.
|
|
123
|
+
|
|
30
124
|
### 0.1.0-next.5
|
|
31
125
|
|
|
32
126
|
No artifact schema or protocol changes. Three behavioral changes:
|
package/PROOF_SDK_CONTRACT.md
CHANGED
|
@@ -9,11 +9,8 @@ instead; this document is for anyone _consuming_ the template.
|
|
|
9
9
|
|
|
10
10
|
> **"Manifest" in this document always means the _mission_ manifest**
|
|
11
11
|
> (`.proof/current-mission.json`) — the file that pins what one PR must prove.
|
|
12
|
-
> It is unrelated to a **module descriptor** (`module.meta.ts` →
|
|
13
|
-
> `.proof/modules.json`), which describes what a module is for and what a buyer
|
|
14
|
-
> must decide before using it. See §3.1 below.
|
|
15
12
|
|
|
16
|
-
- **
|
|
13
|
+
- **Versions:** mission manifests 1; proof traces 2; proof health protocol 1.
|
|
17
14
|
- **Stability:** The shapes in this document are the contract. Additive
|
|
18
15
|
changes (new optional fields, new vocabulary entries) do not bump the
|
|
19
16
|
version; breaking changes bump `schemaVersion` and are documented here.
|
|
@@ -25,6 +22,17 @@ instead; this document is for anyone _consuming_ the template.
|
|
|
25
22
|
|
|
26
23
|
---
|
|
27
24
|
|
|
25
|
+
## Current acceptance semantics
|
|
26
|
+
|
|
27
|
+
The unreleased audit hardening in [COMPATIBILITY.md](COMPATIBILITY.md#unreleased-audit-hardening)
|
|
28
|
+
supersedes older template-specific command examples below. Trace consumers
|
|
29
|
+
must use `decodeTrace`/`decodeTraceBundle` for structural validation and
|
|
30
|
+
`readTraceDirectory` for CLI-equivalent freshness checks. Evidence counts only
|
|
31
|
+
when the proof and step passed, the assertion has a passing consistent verdict,
|
|
32
|
+
and the trace is a baseline rather than a planted mutation. Helper-origin
|
|
33
|
+
metadata is a convention enforced by the writer, not cryptographic attestation.
|
|
34
|
+
The consumer controls and protects the runner and its trust inputs.
|
|
35
|
+
|
|
28
36
|
## 1. The loop
|
|
29
37
|
|
|
30
38
|
```
|
|
@@ -50,7 +58,7 @@ instead; this document is for anyone _consuming_ the template.
|
|
|
50
58
|
│ │ fails cheaply BEFORE the database proofs run │
|
|
51
59
|
│ ├─ proof:verify │
|
|
52
60
|
│ ├─ runs every *.proof.ts spec │
|
|
53
|
-
│ ├─ aggregates .proof/traces/<missionId>.json │
|
|
61
|
+
│ ├─ aggregates .proof/traces/missions/<missionId>.json │
|
|
54
62
|
│ └─ validates manifest requirements against artifacts │
|
|
55
63
|
│ ├─ proof:coverage --strict │
|
|
56
64
|
│ └─ proof:inventory → requires mutation or acceptance │
|
|
@@ -534,7 +542,6 @@ Everything the template produces that the consumer consumes.
|
|
|
534
542
|
| Output | Location | Emitted by | Cadence |
|
|
535
543
|
| --------------------------- | ------------------------------------------------------------------------------ | ---------------------------------------------------- | -------------------------------------- |
|
|
536
544
|
| Capability map | `.proof/capabilities.json` (gitignored) | `pnpm proof:scan` (via `proof:build` in setup/CI) | Every setup + CI run |
|
|
537
|
-
| Module descriptors | `.proof/modules.json` (gitignored) | `pnpm proof:modules` (via `proof:build` in setup/CI) | Every setup + CI run |
|
|
538
545
|
| Schema + RLS classification | `.proof/schema.json` (gitignored) | `pnpm proof:parse` (via `proof:build` in setup/CI) | Every setup + CI run |
|
|
539
546
|
| Proof action allowlist | `app/__shared/proof-runtime/action-registry.generated.ts` | `pnpm proof:registry` | When action requirements/probes change |
|
|
540
547
|
| PR verification status | GitHub check `Proof` | `pnpm proof:check` in `.github/workflows/ci.yml` | Every PR push |
|
|
@@ -714,82 +721,16 @@ from another commit or whose `specFile` no longer exists with `stale_trace`.
|
|
|
714
721
|
|
|
715
722
|
---
|
|
716
723
|
|
|
717
|
-
### 3.1 Module descriptors —
|
|
718
|
-
|
|
719
|
-
`.proof/modules.json` answers a different question from everything else here.
|
|
720
|
-
The trace artifacts say _"this claim was proved"_; this one says _"this is what
|
|
721
|
-
exists, and this is what a buyer has to settle before it can be used"_. It is
|
|
722
|
-
what a planning agent reads instead of the template's source, which is the point:
|
|
723
|
-
without it the planner needs the codebase in its context, and that grows with
|
|
724
|
-
every module we write.
|
|
725
|
-
|
|
726
|
-
Regenerate it with `pnpm proof:modules`. Unlike `schema.json` it needs **no
|
|
727
|
-
Docker, database, or migration aggregate** — it reads the filesystem and
|
|
728
|
-
`capabilities.json`, and takes about a second.
|
|
729
|
-
|
|
730
|
-
Each entry has a declared half and a derived half:
|
|
731
|
-
|
|
732
|
-
```jsonc
|
|
733
|
-
{
|
|
734
|
-
"schemaVersion": 1,
|
|
735
|
-
"modules": [
|
|
736
|
-
{
|
|
737
|
-
// Declared in app/__core/<module>/module.meta.ts
|
|
738
|
-
"id": "user-auth",
|
|
739
|
-
"kind": "core", // core | extension | business | generated
|
|
740
|
-
"purpose": "Signs people in and proves who they are — …",
|
|
741
|
-
"env": [],
|
|
742
|
-
"decisions": [
|
|
743
|
-
{
|
|
744
|
-
"id": "mfa_enforcement", // snake_case, unique across all modules
|
|
745
|
-
"ask": "Must people use a second factor to sign in?",
|
|
746
|
-
"because": "Stricter is safer but adds a step for everyone.",
|
|
747
|
-
"options": [{ "value": "optional", "means": "…" }],
|
|
748
|
-
"applied_by": "config", // config | content | code
|
|
749
|
-
"applies": ["app/__core/user-auth/src/config.ts#MFA_POLICY"],
|
|
750
|
-
},
|
|
751
|
-
],
|
|
752
|
-
"inputs": [
|
|
753
|
-
/* values the buyer supplies rather than picks */
|
|
754
|
-
],
|
|
755
|
-
"defaults": [
|
|
756
|
-
/* what we chose for them, each with a seam */
|
|
757
|
-
],
|
|
758
|
-
|
|
759
|
-
// Derived from source — never written by hand
|
|
760
|
-
"path": "app/__core/user-auth",
|
|
761
|
-
"entities": [], // tables its own migrations create
|
|
762
|
-
"requires": ["config", "user-management"],
|
|
763
|
-
"uses_shared": ["ui", "db-supabase"],
|
|
764
|
-
"actions": [{ "name": "signin", "verb": null, "object": null }],
|
|
765
|
-
"proven": { "specs": 1, "mutations": 0 },
|
|
766
|
-
},
|
|
767
|
-
],
|
|
768
|
-
// Tables no descriptor claims — today, the ones owned by __shared modules.
|
|
769
|
-
"unowned_tables": [
|
|
770
|
-
{
|
|
771
|
-
"table": "audit_logs",
|
|
772
|
-
"module": "audit-logging",
|
|
773
|
-
"path": "app/__shared/audit-logging",
|
|
774
|
-
},
|
|
775
|
-
],
|
|
776
|
-
}
|
|
777
|
-
```
|
|
778
|
-
|
|
779
|
-
**Two audiences, one file.** A planner reads `purpose`, `decisions`, `inputs`
|
|
780
|
-
and `applied_by` — business language, no paths, roughly 80 tokens per decision.
|
|
781
|
-
The agent that _applies_ an answer additionally reads `applies`, which names
|
|
782
|
-
every place the answer lands, including files in other modules.
|
|
783
|
-
|
|
784
|
-
**Every claim is checked.** `pnpm proof:modules:check` fails when a module has
|
|
785
|
-
no descriptor, when an id does not match its directory, when a decision has
|
|
786
|
-
fewer than two options or a duplicate id, when a declared environment variable
|
|
787
|
-
is undocumented or an undeclared one is read, and — the one that matters most in
|
|
788
|
-
practice — **when a seam no longer exists**. A moved file fails CI instead of
|
|
789
|
-
quietly sending the next agent to the wrong place.
|
|
724
|
+
### 3.1 Module descriptors — moved to the consumer
|
|
790
725
|
|
|
791
|
-
|
|
792
|
-
|
|
726
|
+
Until `0.1.0-next.5` the package also shipped a module-descriptor system
|
|
727
|
+
(`module.meta.ts` → `.proof/modules.json`, plus the `modules` and
|
|
728
|
+
`modules-check` commands). It served the consumer's *planner* — what a module
|
|
729
|
+
is for, what a buyer must decide, where answers land — and never fed
|
|
730
|
+
verification, so as of `0.1.0-next.6` it is consumer-owned and no longer part
|
|
731
|
+
of this package. The last release carrying it is `proof-harness@0.1.0-next.5`
|
|
732
|
+
(Apache-2.0), whose implementation is the reference for consumers vendoring
|
|
733
|
+
the system into their own repository.
|
|
793
734
|
|
|
794
735
|
## 4. Failure contract
|
|
795
736
|
|
|
@@ -956,13 +897,6 @@ operation }` assertion has neither a matching mutation nor a valid entry in
|
|
|
956
897
|
`seed.workspace` writes only `name`; a schema that requires more states it
|
|
957
898
|
via `seed.workspace(name, { columns })` or `workspaceColumns` on
|
|
958
899
|
`assert.tenantIsolation`.
|
|
959
|
-
- **Accepted descriptor gaps expire.** `proof:modules:check` accepts an
|
|
960
|
-
undescribed required-root module only through `.proof/module-policy.json`
|
|
961
|
-
(`schemaVersion: 1`, `acceptedUndescribed: [{ module, reason }]`), each
|
|
962
|
-
entry carrying a written reason, and prints the accepted backlog on every
|
|
963
|
-
run. An acceptance whose module gains a descriptor or no longer exists
|
|
964
|
-
fails as `module_policy_stale`, so paid-down debt leaves the file rather
|
|
965
|
-
than lingering as invisible permission.
|
|
966
900
|
- **A mutation that is not demonstrably live cannot judge a proof.** The
|
|
967
901
|
mutation runner never (re)applies migrations; it starts (or adopts) the
|
|
968
902
|
shared dev server before the first defect is planted, then reads each
|
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.
|
|
20
|
+
pnpm add proof-harness@0.1.0-next.6
|
|
21
21
|
```
|
|
22
22
|
|
|
23
23
|
The package exposes environment-specific entry points:
|
|
@@ -39,8 +39,8 @@ on:
|
|
|
39
39
|
|
|
40
40
|
- **Assumes nothing about your schema** — trace recording and the assertion
|
|
41
41
|
vocabulary, mission validation, coverage and its ratchet, drift, mutation
|
|
42
|
-
testing,
|
|
43
|
-
|
|
42
|
+
testing, and the CLI. These read your generated schema and config, or take
|
|
43
|
+
what you give them.
|
|
44
44
|
- **Assumes the conventional Supabase/workspace layout** — `seed.*` (a
|
|
45
45
|
`workspaces` table with a `name` column, a `workspace_members` join table,
|
|
46
46
|
and a `public.users` mirror maintained by a `handle_new_user` trigger) and
|
|
@@ -61,8 +61,8 @@ code via `allowProofServiceHosts`; there is deliberately no environment
|
|
|
61
61
|
override.
|
|
62
62
|
|
|
63
63
|
The package also provides one `proof-harness` executable with `scan`, `parse`,
|
|
64
|
-
`registry`, `build`, `verify`, `coverage`, `inventory`, `drift`, `
|
|
65
|
-
|
|
64
|
+
`registry`, `build`, `verify`, `coverage`, `inventory`, `drift`, and `mutate`
|
|
65
|
+
subcommands. Commands read repository paths from
|
|
66
66
|
`proof.config.mjs`; without one they use a conventional application layout.
|
|
67
67
|
Product migration aggregation and explicit mutation definitions remain
|
|
68
68
|
repository-owned trust inputs rather than package defaults.
|
|
@@ -91,3 +91,17 @@ Licensed under the [Apache License 2.0](./LICENSE).
|
|
|
91
91
|
The package is released from this public repository with npm provenance.
|
|
92
92
|
Prerelease consumers must pin an exact version rather than using `next`, `^`, or
|
|
93
93
|
`~`; see [COMPATIBILITY.md](./COMPATIBILITY.md) for the compatibility policy.
|
|
94
|
+
|
|
95
|
+
## Audit hardening (0.1.0-next.7, unreleased)
|
|
96
|
+
|
|
97
|
+
The working branch adds shared fail-closed trace ingestion, source freshness,
|
|
98
|
+
stronger authorization probes, isolated mutation evidence and interruption
|
|
99
|
+
recovery, conservative discovery, and package-owned drift regeneration.
|
|
100
|
+
See [required migration changes](COMPATIBILITY.md#unreleased-audit-hardening)
|
|
101
|
+
before upgrading a consumer. In particular, existing traces must be rerun and
|
|
102
|
+
unsupported SQL now blocks assessment.
|
|
103
|
+
|
|
104
|
+
The harness establishes specific tested claims. Acceptance still requires
|
|
105
|
+
consumer-owned requirements and a trusted CI job that controls the harness,
|
|
106
|
+
policies, and evidence. A passing trace is not a general proof of code quality
|
|
107
|
+
or a defense against an executor that can rewrite the acceptance gate.
|
package/cli/config.mjs
CHANGED
|
@@ -6,11 +6,15 @@ const DEFAULT_CONFIG = Object.freeze({
|
|
|
6
6
|
artifacts: {
|
|
7
7
|
capabilities: ".proof/capabilities.json",
|
|
8
8
|
schema: ".proof/schema.json",
|
|
9
|
-
modules: ".proof/modules.json",
|
|
10
9
|
traces: ".proof/traces",
|
|
11
10
|
drift: ".proof/drift.json",
|
|
12
11
|
mutations: ".proof/mutations",
|
|
13
12
|
},
|
|
13
|
+
// The module-descriptor system (module.meta.ts → modules.json and its
|
|
14
|
+
// checker) moved to the consumer template in 0.1.0-next.6: it serves the
|
|
15
|
+
// planner, which is product, not verification. Its former config keys
|
|
16
|
+
// (artifacts.modules, policies.module, roots.modules, roots.sharedModules,
|
|
17
|
+
// moduleKinds) are ignored if a proof.config.mjs still supplies them.
|
|
14
18
|
mission: {
|
|
15
19
|
current: ".proof/current-mission.json",
|
|
16
20
|
directory: ".proof/missions",
|
|
@@ -19,13 +23,10 @@ const DEFAULT_CONFIG = Object.freeze({
|
|
|
19
23
|
coverage: ".proof/coverage-policy.json",
|
|
20
24
|
mutation: ".proof/mutation-policy.json",
|
|
21
25
|
migration: ".proof/migration-policy.json",
|
|
22
|
-
module: ".proof/module-policy.json",
|
|
23
26
|
},
|
|
24
27
|
roots: {
|
|
25
28
|
source: "app",
|
|
26
29
|
actions: ["app/__core", "app/__business-logic", "app/__extensions"],
|
|
27
|
-
modules: ["app/__core", "app/__extensions", "app/__business-logic"],
|
|
28
|
-
sharedModules: "app/__shared",
|
|
29
30
|
proofs: "e2e/proofs",
|
|
30
31
|
fixtures: "e2e/fixtures",
|
|
31
32
|
migrations: "supabase/migrations",
|
|
@@ -42,13 +43,9 @@ const DEFAULT_CONFIG = Object.freeze({
|
|
|
42
43
|
envExample: ".env.local.example",
|
|
43
44
|
supabaseConfig: "supabase/config.toml",
|
|
44
45
|
},
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
{ root: "app/__extensions", kind: "extension" },
|
|
49
|
-
],
|
|
50
|
-
optional: [{ root: "app/__business-logic", kind: "business" }],
|
|
51
|
-
},
|
|
46
|
+
// Optional consumer-owned argv used to prepare generated migration inputs
|
|
47
|
+
// in both the working tree and the archived base before parsing.
|
|
48
|
+
driftPrepare: null,
|
|
52
49
|
driftSources: ["app", "package.json"],
|
|
53
50
|
mutationCatalog: null,
|
|
54
51
|
});
|
|
@@ -66,10 +63,6 @@ function mergeConfig(config = {}) {
|
|
|
66
63
|
...DEFAULT_CONFIG.registryAliases,
|
|
67
64
|
...config.registryAliases,
|
|
68
65
|
},
|
|
69
|
-
moduleKinds: {
|
|
70
|
-
...DEFAULT_CONFIG.moduleKinds,
|
|
71
|
-
...config.moduleKinds,
|
|
72
|
-
},
|
|
73
66
|
};
|
|
74
67
|
}
|
|
75
68
|
|
|
@@ -115,3 +108,29 @@ export async function loadMutationCatalog(config) {
|
|
|
115
108
|
}
|
|
116
109
|
|
|
117
110
|
export { DEFAULT_CONFIG };
|
|
111
|
+
|
|
112
|
+
/** Derived outputs are excluded from the source identity, never trust inputs. */
|
|
113
|
+
export function evidenceExclusions(config) {
|
|
114
|
+
return [
|
|
115
|
+
...Object.values(config.artifacts),
|
|
116
|
+
`${path.dirname(config.artifacts.mutations)}/mutation-recovery.json`,
|
|
117
|
+
].map((p) => path.resolve(config.rootDir, p));
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** Reject a trace/output directory that resolves to the repository or its parent. */
|
|
121
|
+
export function assertArtifactDirectory(directory, rootDir) {
|
|
122
|
+
function canonical(file) {
|
|
123
|
+
const absolute = path.resolve(file);
|
|
124
|
+
if (fs.existsSync(absolute)) return fs.realpathSync(absolute);
|
|
125
|
+
const parent = path.dirname(absolute);
|
|
126
|
+
return path.join(canonical(parent), path.basename(absolute));
|
|
127
|
+
}
|
|
128
|
+
const output = canonical(directory);
|
|
129
|
+
const root = canonical(rootDir);
|
|
130
|
+
const relative = path.relative(output, root);
|
|
131
|
+
if (!relative || (!relative.startsWith("..") && !path.isAbsolute(relative)))
|
|
132
|
+
throw new Error(
|
|
133
|
+
"[PROOF_FAIL] unsafe_artifact_directory: output must not be the repository or an ancestor",
|
|
134
|
+
);
|
|
135
|
+
return output;
|
|
136
|
+
}
|
|
@@ -269,6 +269,7 @@ function parsePolicies(sql, sourceFile) {
|
|
|
269
269
|
name: policyName,
|
|
270
270
|
table: tableName,
|
|
271
271
|
command,
|
|
272
|
+
mode: /\bAS\s+RESTRICTIVE\b/i.test(rest) ? "RESTRICTIVE" : "PERMISSIVE",
|
|
272
273
|
roles,
|
|
273
274
|
using: usingBody.trim(),
|
|
274
275
|
check: checkBody.trim(),
|
|
@@ -308,7 +309,7 @@ function policyIndicatesUser(policy) {
|
|
|
308
309
|
}
|
|
309
310
|
|
|
310
311
|
function policyIsPublicRead(policy) {
|
|
311
|
-
if (policy.command !== "SELECT"
|
|
312
|
+
if (policy.command !== "SELECT") return false;
|
|
312
313
|
const body = policy.using.trim().toLowerCase();
|
|
313
314
|
return body === "true";
|
|
314
315
|
}
|
|
@@ -348,9 +349,7 @@ function classifyTable(policies) {
|
|
|
348
349
|
if (
|
|
349
350
|
readPolicies.length > 0 &&
|
|
350
351
|
readPolicies.every(policyIsPublicRead) &&
|
|
351
|
-
authPolicies.every(
|
|
352
|
-
(p) => p.command === "SELECT" || p.command === "ALL" || p.check === "",
|
|
353
|
-
)
|
|
352
|
+
authPolicies.every((p) => p.command === "SELECT")
|
|
354
353
|
) {
|
|
355
354
|
return "public_read";
|
|
356
355
|
}
|
|
@@ -366,6 +365,26 @@ function classifyTable(policies) {
|
|
|
366
365
|
|
|
367
366
|
export function main() {
|
|
368
367
|
const sources = readAllMigrations();
|
|
368
|
+
const unsupported = [];
|
|
369
|
+
const enabledTables = new Set();
|
|
370
|
+
for (const { file, sql } of sources) {
|
|
371
|
+
// This parser is intentionally conservative. Never preserve historical CREATE
|
|
372
|
+
// facts as current evidence after an operation we cannot interpret.
|
|
373
|
+
const enable =
|
|
374
|
+
/ALTER\s+TABLE\s+(?:ONLY\s+)?(?:"?public"?\.)?"?(\w+)"?\s+ENABLE\s+ROW\s+LEVEL\s+SECURITY\s*;/gi;
|
|
375
|
+
for (const match of sql.matchAll(enable)) enabledTables.add(match[1]);
|
|
376
|
+
const remainder = sql.replace(enable, "");
|
|
377
|
+
if (
|
|
378
|
+
/\b(?:ALTER\s+(?:TABLE|POLICY)|DROP\s+(?:TABLE|POLICY)|DO\s+\$|EXECUTE\s+)/i.test(
|
|
379
|
+
remainder,
|
|
380
|
+
)
|
|
381
|
+
)
|
|
382
|
+
unsupported.push({
|
|
383
|
+
file,
|
|
384
|
+
reason:
|
|
385
|
+
"migration changes schema or policy state using unsupported SQL; final schema is unassessable",
|
|
386
|
+
});
|
|
387
|
+
}
|
|
369
388
|
const tablesByName = new Map();
|
|
370
389
|
const policiesByTable = new Map();
|
|
371
390
|
|
|
@@ -373,7 +392,10 @@ export function main() {
|
|
|
373
392
|
for (const t of extractTables(sql, file)) {
|
|
374
393
|
const existing = tablesByName.get(t.name);
|
|
375
394
|
if (existing) {
|
|
376
|
-
|
|
395
|
+
unsupported.push({
|
|
396
|
+
file,
|
|
397
|
+
reason: `repeated CREATE TABLE ${t.name} cannot establish final columns`,
|
|
398
|
+
});
|
|
377
399
|
existing.columns = [...new Set([...existing.columns, ...t.columns])];
|
|
378
400
|
if (!existing.files.includes(file)) existing.files.push(file);
|
|
379
401
|
} else {
|
|
@@ -395,17 +417,23 @@ export function main() {
|
|
|
395
417
|
|
|
396
418
|
for (const [name, entry] of tablesByName) {
|
|
397
419
|
const policies = policiesByTable.get(name) ?? [];
|
|
398
|
-
const classification =
|
|
420
|
+
const classification = enabledTables.has(name)
|
|
421
|
+
? classifyTable(policies)
|
|
422
|
+
: "unclassified";
|
|
399
423
|
|
|
400
424
|
const normalized = {
|
|
401
425
|
name,
|
|
402
426
|
columns: [...entry.columns].sort(),
|
|
403
427
|
rls_classification: classification,
|
|
428
|
+
rls_enabled: enabledTables.has(name),
|
|
404
429
|
policies: policies
|
|
405
430
|
.map((p) => ({
|
|
406
431
|
name: p.name,
|
|
407
432
|
command: p.command,
|
|
408
433
|
roles: [...p.roles].sort(),
|
|
434
|
+
using: p.using,
|
|
435
|
+
check: p.check,
|
|
436
|
+
mode: p.mode,
|
|
409
437
|
}))
|
|
410
438
|
.sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0)),
|
|
411
439
|
sourceFiles: [...entry.files]
|
|
@@ -429,6 +457,8 @@ export function main() {
|
|
|
429
457
|
|
|
430
458
|
const output = {
|
|
431
459
|
schemaVersion: 1,
|
|
460
|
+
assessed: unsupported.length === 0,
|
|
461
|
+
issues: unsupported,
|
|
432
462
|
tables,
|
|
433
463
|
unclassified: unclassifiedTables,
|
|
434
464
|
};
|
|
@@ -439,6 +469,10 @@ export function main() {
|
|
|
439
469
|
console.log(
|
|
440
470
|
`[proof:parse] parsed ${sources.length} migration file(s); wrote ${tables.length} table(s), ${unclassifiedTables.length} unclassified to ${OUTPUT_PATH}`,
|
|
441
471
|
);
|
|
472
|
+
if (unsupported.length)
|
|
473
|
+
throw new Error(
|
|
474
|
+
`[PROOF_FAIL] schema_unassessed: ${unsupported.map((p) => `${p.file}: ${p.reason}`).join("; ")}`,
|
|
475
|
+
);
|
|
442
476
|
if (unclassifiedTables.length > 0) {
|
|
443
477
|
console.log(`[proof:parse] unclassified tables:`);
|
|
444
478
|
for (const u of unclassifiedTables)
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import { readTraceDirectory, baselineAssertions } from "../../dist/node.js";
|
|
2
3
|
// ---------------------------------------------------------------------------
|
|
3
4
|
// proof_coverage.mjs
|
|
4
5
|
//
|
|
@@ -57,7 +58,7 @@ import {
|
|
|
57
58
|
TENANT_ISOLATION_ACTION_HELPERS,
|
|
58
59
|
TENANT_ISOLATION_TABLE_HELPERS,
|
|
59
60
|
} from "../../dist/portable-vocabulary.js";
|
|
60
|
-
import { loadProofConfig } from "../config.mjs";
|
|
61
|
+
import { loadProofConfig, evidenceExclusions } from "../config.mjs";
|
|
61
62
|
|
|
62
63
|
const CONFIG = await loadProofConfig();
|
|
63
64
|
process.chdir(CONFIG.rootDir);
|
|
@@ -121,22 +122,11 @@ function readJson(file, { required = true } = {}) {
|
|
|
121
122
|
|
|
122
123
|
/** Every assertion from every trace, flattened. Steps may nest assertions. */
|
|
123
124
|
function readAssertions() {
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
for (const artifact of Array.isArray(parsed) ? parsed : [parsed]) {
|
|
130
|
-
// The aggregate <missionId>.json embeds every per-spec trace; skip it so
|
|
131
|
-
// assertions are not counted twice.
|
|
132
|
-
if (Array.isArray(artifact?.traces)) continue;
|
|
133
|
-
const nested = (artifact?.steps ?? []).flatMap((s) => s.assertions ?? []);
|
|
134
|
-
for (const assertion of [...(artifact?.assertions ?? []), ...nested]) {
|
|
135
|
-
out.push({ ...assertion, proofId: artifact?.proofId ?? null });
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
return out;
|
|
125
|
+
return baselineAssertions(
|
|
126
|
+
readTraceDirectory(TRACES_DIR, {
|
|
127
|
+
excludedPaths: evidenceExclusions(CONFIG),
|
|
128
|
+
}),
|
|
129
|
+
);
|
|
140
130
|
}
|
|
141
131
|
|
|
142
132
|
// ---------------------------------------------------------------------------
|
|
@@ -202,6 +192,10 @@ function buildReport() {
|
|
|
202
192
|
const policy = readJson(POLICY_PATH, { required: false }) ?? {
|
|
203
193
|
acceptedGaps: [],
|
|
204
194
|
};
|
|
195
|
+
if (schema.assessed === false || capabilities.unclassified?.length)
|
|
196
|
+
throw new Error(
|
|
197
|
+
"[PROOF_FAIL] discovery_unassessed: schema or capability discovery is incomplete",
|
|
198
|
+
);
|
|
205
199
|
const assertions = readAssertions();
|
|
206
200
|
|
|
207
201
|
const tableNames = new Set((schema.tables ?? []).map((t) => t.name));
|