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 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.5"
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:
@@ -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
- - **Version:** 1 (`schemaVersion` in every manifest and artifact)
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 — what exists, and what must be decided
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
- Circular dependencies in the derived graph are **reported, not failed**, while a
792
- known inversion is being resolved.
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.5
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, module checks, and the CLI. These read your generated schema and
43
- config, or take what you give them.
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`, `modules`,
65
- `modules-check`, and `mutate` subcommands. Commands read repository paths from
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
- moduleKinds: {
46
- required: [
47
- { root: "app/__core", kind: "core" },
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" && policy.command !== "ALL") return false;
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
- // Merge columns from ALTER TABLE or re-declarations (future-proof).
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 = classifyTable(policies);
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
- if (!fs.existsSync(TRACES_DIR)) return [];
125
- const out = [];
126
- for (const file of fs.readdirSync(TRACES_DIR)) {
127
- if (!file.endsWith(".json")) continue;
128
- const parsed = readJson(path.join(TRACES_DIR, file));
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));