proof-harness 0.1.0-next.1

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.
Files changed (53) hide show
  1. package/COMPATIBILITY.md +26 -0
  2. package/LICENSE +201 -0
  3. package/PROOF_SDK_CONTRACT.md +1255 -0
  4. package/README.md +60 -0
  5. package/cli/config.mjs +116 -0
  6. package/cli/engines/generate_proof_action_registry.mjs +271 -0
  7. package/cli/engines/parse_proof_schema.mjs +457 -0
  8. package/cli/engines/proof_coverage.mjs +871 -0
  9. package/cli/engines/proof_drift.mjs +980 -0
  10. package/cli/engines/proof_module_check.mjs +498 -0
  11. package/cli/engines/proof_mutation_check.mjs +1084 -0
  12. package/cli/engines/proof_mutation_inventory.mjs +508 -0
  13. package/cli/engines/proof_source_mutation.mjs +35 -0
  14. package/cli/engines/proof_verify.mjs +620 -0
  15. package/cli/engines/scan_module_meta.mjs +352 -0
  16. package/cli/engines/scan_proof_capabilities.mjs +359 -0
  17. package/cli/proof-harness.mjs +107 -0
  18. package/dist/action-types-C3OmHRyH.d.ts +26 -0
  19. package/dist/fixture-B-dmnG5G.d.ts +105 -0
  20. package/dist/mission-types-Dr5rBHYO.d.ts +299 -0
  21. package/dist/node.d.ts +79 -0
  22. package/dist/node.js +553 -0
  23. package/dist/node.js.map +1 -0
  24. package/dist/playwright.d.ts +433 -0
  25. package/dist/playwright.js +1824 -0
  26. package/dist/playwright.js.map +1 -0
  27. package/dist/portable-vocabulary.d.ts +86 -0
  28. package/dist/portable-vocabulary.js +43 -0
  29. package/dist/portable-vocabulary.js.map +1 -0
  30. package/dist/server.d.ts +55 -0
  31. package/dist/server.js +376 -0
  32. package/dist/server.js.map +1 -0
  33. package/dist/shared.d.ts +169 -0
  34. package/dist/shared.js +121 -0
  35. package/dist/shared.js.map +1 -0
  36. package/dist/trace-types-DkIzq-l9.d.ts +333 -0
  37. package/package.json +109 -0
  38. package/proof-consumer-fixtures/README.md +60 -0
  39. package/proof-consumer-fixtures/aggregates/M-CONFORMANCE-FAIL.json +112 -0
  40. package/proof-consumer-fixtures/aggregates/M-CONFORMANCE-PASS.json +101 -0
  41. package/proof-consumer-fixtures/baseline/failing/widgets-tenant-isolation.json +46 -0
  42. package/proof-consumer-fixtures/baseline/passing/widgets-tenant-isolation.json +45 -0
  43. package/proof-consumer-fixtures/corpus.json +102 -0
  44. package/proof-consumer-fixtures/duplicates/first/widgets-tenant-isolation.json +12 -0
  45. package/proof-consumer-fixtures/duplicates/second/widgets-tenant-isolation.json +12 -0
  46. package/proof-consumer-fixtures/inputs/capabilities.json +4 -0
  47. package/proof-consumer-fixtures/inputs/mission.json +20 -0
  48. package/proof-consumer-fixtures/inputs/schema.json +4 -0
  49. package/proof-consumer-fixtures/malformed/invalid-trace.json.invalid +4 -0
  50. package/proof-consumer-fixtures/malformed/wrong-manifest-version.json +11 -0
  51. package/proof-consumer-fixtures/malformed/wrong-trace-shape.json +8 -0
  52. package/proof-consumer-fixtures/mutations/MUT-WIDGETS-LEAK/mutation.json +17 -0
  53. package/proof-consumer-fixtures/mutations/MUT-WIDGETS-LEAK/widgets-tenant-isolation.json +50 -0
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/shared/portable-vocabulary.mjs","../src/shared/mission-types.ts","../src/shared/vocabulary.ts","../src/shared/trace-types.ts","../src/node/validate-mission.ts"],"sourcesContent":["// Import External Packages\n// Import Local Imports\n// Import Core Dependencies\n// Import Shared Dependencies\n// Import Extension Dependencies\n\n// ---------------------------------------------------------------------------\n// Portable proof vocabulary — the single source of truth for closed value\n// lists that BOTH the TypeScript SDK and the dependency-free Node scripts\n// (`proof_verify.mjs`, `proof_drift.mjs`, `proof_coverage.mjs`) must agree on.\n//\n// Why a .mjs file inside a TypeScript package: those scripts run under plain\n// `node` with no compile step and must stay fast-starting, so they cannot\n// import `vocabulary.ts`. Everything here is therefore plain JS, and the\n// TypeScript side derives its union types from these arrays\n// (`(typeof ACTION_CHANGE_KINDS)[number]` in mission-types.ts), so adding or\n// renaming a value in exactly one place updates the validators, the drift\n// script, coverage, and the types together. The JSDoc `@type {const}` casts\n// are what preserve the literal element types for that derivation.\n//\n// Only vocabularies needed by the .mjs scripts live here; TS-only\n// vocabularies stay in `vocabulary.ts`.\n// ---------------------------------------------------------------------------\n\n/**\n * Change kinds an `expectedChanges.actions[]` declaration may allow, and the\n * facets a modified action decomposes into. Like tables, EVERY facet of a\n * change must be declared for the change to be covered — a mission expecting\n * an invariant declaration to change does not thereby authorize an RBAC/auth\n * middleware change on the same action.\n *\n * - `middleware_changed`: a scanner-observed guard (auth / tenantIsolation\n * / rbac) appeared, disappeared, or flipped.\n * - `invariants_changed`: the `withProof({ invariants })` list changed.\n * - `service_role_mutations_changed`: the set of tables the action writes\n * through the service-role client changed.\n * - `metadata_changed`: verb, object, acceptsWorkspaceId, internalOnly, or\n * usesDirectUpdateTag changed.\n */\nexport const ACTION_CHANGE_KINDS = /** @type {const} */ ([\n \"added\",\n \"removed\",\n \"middleware_changed\",\n \"invariants_changed\",\n \"service_role_mutations_changed\",\n \"metadata_changed\",\n]);\n\n/**\n * Change kinds an `expectedChanges.tables[]` declaration may allow, and the\n * facets a modified table decomposes into. Same coverage rule as actions:\n * declaring `columns_added` on a table does not authorize a policy change\n * that rode along on the same table.\n */\nexport const TABLE_CHANGE_KINDS = /** @type {const} */ ([\n \"added\",\n \"removed\",\n \"columns_added\",\n \"columns_removed\",\n \"rls_classification_changed\",\n \"policies_changed\",\n]);\n\n/**\n * The only helpers whose `emittedBy` stamp counts as tenant-isolation\n * evidence, split by target.\n *\n * Identity matters because helpers differ in who chooses the `kind`:\n * `assert.httpResponse` records the caller's kind verbatim, so its stamp on a\n * `tenant_isolation` assertion would prove only that an HTTP probe ran — not\n * that anything checked cross-tenant visibility. Isolation OF A TABLE is\n * exactly what `assert.tenantIsolation` proves, so table targets accept only\n * it; action-ref targets also accept `assert.authorization`, whose action\n * probes record the cross-tenant invariant they actually establish.\n */\nexport const TENANT_ISOLATION_TABLE_HELPERS = /** @type {const} */ ([\n \"assert.tenantIsolation\",\n]);\n\n/** Allowed stamps for tenant-isolation claims against non-table targets (action refs). */\nexport const TENANT_ISOLATION_ACTION_HELPERS = /** @type {const} */ ([\n \"assert.tenantIsolation\",\n \"assert.authorization\",\n]);\n","// ---------------------------------------------------------------------------\n// Mission manifest types.\n//\n// A *mission manifest* is the JSON artifact an external orchestrator or a\n// developer in manual mode writes to `.proof/current-mission.json` to\n// tell `pnpm proof:verify` what the PR must prove. The validator at\n// `src/node/validate-mission.ts` cross-references each\n// requirement against the fresh `.proof/capabilities.json`, `.proof/schema.json`,\n// and the aggregated trace.\n//\n// Shape is pinned by contract — additive changes bump nothing, breaking\n// changes bump `schemaVersion` at the top of the manifest. Consumers should\n// assert `schemaVersion === 1` and fail loudly otherwise.\n// ---------------------------------------------------------------------------\n\nimport type { AssertionRole } from \"./trace-types\";\nimport {\n ACTION_CHANGE_KINDS,\n TABLE_CHANGE_KINDS,\n} from \"./portable-vocabulary.mjs\";\nimport type {\n PolicyCommand,\n ProofKind,\n ProofVerb,\n RlsClassification,\n} from \"./vocabulary\";\n\n/**\n * Manifest schema version. Deliberately still 1 after the July 2026 validator\n * work: `trace_must_prove[].role` and `.proofId` are OPTIONAL additions, so\n * every v1 manifest is still shape-valid and consumers need not regenerate\n * anything. Note that validation did get stricter — a `control` assertion can\n * no longer satisfy a requirement — so a manifest that passed on a vacuous\n * probe before may now (correctly) fail. That is a behaviour tightening, not a\n * shape change, and bumping the version would have forced a rewrite of every\n * existing manifest for no benefit.\n */\nexport const MISSION_MANIFEST_SCHEMA_VERSION = 1;\n\nexport interface CapabilityRequirement {\n /** Matches `createAction({ functionName: <name> })` exactly. */\n name: string;\n /** Module folder name (e.g. `\"issues\"`, not `\"__business-logic/issues\"`). */\n module: string;\n /** One of PROOF_VERBS. */\n verb: ProofVerb;\n /** Free-form noun, lowercased by convention (e.g. `\"issue\"`, `\"workspace\"`). */\n object: string;\n /** Optional: invariants that the action's `withProof({ invariants })` must advertise. */\n invariants?: readonly string[];\n}\n\nexport interface SchemaRequirement {\n /** Unqualified public-schema table name. */\n table: string;\n /** Columns that must exist on the table. Order-insensitive. */\n required_columns: readonly string[];\n /** One of RLS_CLASSIFICATIONS. */\n rls_classification: RlsClassification;\n}\n\n/**\n * \"No policy on this table targets this role for these commands.\"\n *\n * Exists because `rls_classification` is one label per table and Postgres RLS is\n * per-command: `audit_logs` being \"admin readable, service writable\" cannot be\n * said with a single label, and the label a manifest picks would have to be a\n * lie in one direction or the other.\n *\n * Checks reachability, not the predicate — `USING` and `WITH CHECK` are not read.\n * A policy `TO authenticated USING (is_super_admin())` counts as targeting\n * `authenticated`, so \"this role may act only under condition X\" is not\n * expressible here and belongs in `trace_must_prove`, where something exercises\n * it. Write prohibitions (\"members never INSERT into the audit log\") are the\n * natural fit.\n *\n * Deliberately only the prohibition, never \"a policy must exist\". A policy's\n * existence is weak evidence — it says a rule was written, not that it works —\n * and the thing that makes a permission real is a passing proof, which belongs in\n * `trace_must_prove`. A prohibition is different: it is a claim that can fail\n * dangerously and silently, and it is checkable from the migrations alone.\n */\nexport interface PolicyProhibition {\n /** Unqualified public-schema table name. */\n table: string;\n /**\n * Database role that must not be granted access, e.g. `\"authenticated\"` or\n * `\"anon\"`. A policy with no `TO` clause defaults to `PUBLIC` in Postgres and\n * therefore counts as granting every role.\n */\n role: string;\n /**\n * Commands the prohibition covers. Omit to mean \"any command\". A policy\n * declared `FOR ALL` matches whatever is listed here, because that is what it\n * does in the database.\n */\n commands?: readonly PolicyCommand[];\n /** Human-readable statement of the rule, echoed in the failure message. */\n description: string;\n}\n\nexport interface TraceRequirement {\n /** One of PROOF_KINDS. */\n kind: ProofKind;\n /**\n * Free-form identifier the assertion emits. Must match an assertion's\n * `target` exactly (e.g. table name for `tenant_isolation`, capability\n * name for `authorization`).\n */\n target: string;\n /** Human-readable statement of what should be proved. */\n description: string;\n /**\n * Which evidential role must satisfy this requirement. Defaults to\n * `\"primary\"`, which is almost always what you want: a `\"control\"`\n * assertion passes by design and proves only that the probe was capable of\n * failing, so it must never stand in for the claim itself.\n *\n * Accepts the deprecated spellings `\"negative\"` / `\"positive_control\"`.\n */\n role?: AssertionRole;\n /**\n * Optional: restrict matching to assertions emitted by this proof.\n *\n * By default a requirement may be satisfied by a matching assertion from\n * ANY proof in the run. That is convenient but means an unrelated spec can\n * unknowingly satisfy a mission's requirement. Set this when the mission\n * cares that a SPECIFIC proof made the claim. Either way, the aggregated\n * trace records which proof actually supplied the evidence.\n */\n proofId?: string;\n /**\n * What origin of assertion may satisfy this requirement.\n *\n * - `\"helper\"` — only assertions stamped `emittedBy` by an SDK `assert.*`\n * helper count. Helper assertions went through the SDK's vacuity\n * controls and ground-truth re-reads; a spec-recorded assertion asserts\n * whatever the spec wrote into it.\n * - `\"any\"` — spec-recorded assertions (raw `recordAssertion`) count too.\n * Those claims remain backstopped by the mutation inventory, which is\n * why bespoke probes (RPC grants, column guards) stay expressible.\n *\n * `tenant_isolation` is always `\"helper\"`, and the stamp is additionally\n * identity-checked (see TENANT_ISOLATION_TABLE_HELPERS in trace-types):\n * declaring `evidence: \"any\"` for it is a `manifest_shape` error, so the\n * mission gate and strict coverage can never disagree about whether a raw\n * isolation assertion is admissible. Every other kind defaults to `\"any\"`.\n */\n evidence?: EvidenceOrigin;\n}\n\n/** Origin of assertion allowed to satisfy a `trace_must_prove` entry. */\nexport type EvidenceOrigin = \"helper\" | \"any\";\n\n/**\n * The evidence origin a requirement demands, applying the kind-based default.\n * Consumed directly by the compiled validator used by `proof-harness verify`.\n */\nexport function requiredEvidenceOrigin(\n kind: ProofKind,\n evidence: EvidenceOrigin | undefined,\n): EvidenceOrigin {\n // Tenant isolation is helper-only regardless of what the manifest says: the\n // shape validator rejects `evidence: \"any\"` for it, and this belt-and-braces\n // keeps the matching honest even for a manifest that skipped shape checks.\n if (kind === \"tenant_isolation\") return \"helper\";\n return evidence ?? \"any\";\n}\n\nexport interface MissionRequirements {\n capabilities_must_exist: readonly CapabilityRequirement[];\n schema_must_contain: readonly SchemaRequirement[];\n trace_must_prove: readonly TraceRequirement[];\n /**\n * Optional, so every existing v1 manifest stays shape-valid. Omitting it means\n * \"no policy-level claims\", not \"policies are fine\".\n */\n policies_must_not_allow?: readonly PolicyProhibition[];\n}\n\n/**\n * Change kinds an action declaration may allow. Like tables, a modified\n * action is decomposed into facets and EVERY facet must be covered — a\n * mission expecting an invariant declaration to change does not thereby\n * authorize an RBAC/auth middleware change on the same action.\n *\n * Derived from the single runtime source in\n * [`portable-vocabulary.mjs`](./portable-vocabulary.mjs), which the\n * dependency-free Node scripts import directly; see there for the per-facet\n * meanings.\n */\nexport type ActionChangeKind = (typeof ACTION_CHANGE_KINDS)[number];\n\n/**\n * Change kinds a table declaration may allow. A \"modified\" table entry is\n * decomposed into facets, and EVERY facet must be covered by the declaration —\n * declaring `columns_added` on a table does not authorize a policy change on\n * the same table. Derived from `portable-vocabulary.mjs`.\n */\nexport type TableChangeKind = (typeof TABLE_CHANGE_KINDS)[number];\n\n// Re-exported so TS consumers get the runtime lists from the same import\n// site as the types they parameterize.\nexport { ACTION_CHANGE_KINDS, TABLE_CHANGE_KINDS };\n\n/** Exact-ref action declaration. A bare string ref means \"any change\". */\nexport interface ExpectedActionChange {\n /** `<module>:<name>`, the same ref capabilities.json uses. */\n ref: string;\n /** Omit for \"any change to this action\". */\n changes?: readonly ActionChangeKind[];\n}\n\n/** Table declaration. A bare string name means \"any tracked change\". */\nexport interface ExpectedTableChange {\n name: string;\n /** Omit for \"any tracked change to this table\". */\n changes?: readonly TableChangeKind[];\n}\n\n/**\n * The proof-surface change budget a mission grants its PR.\n *\n * Everything else in the manifest states what MUST exist; this block bounds\n * what else MAY change on the surfaces drift tracks. Read by\n * the package drift engine, which diffs the regenerated\n * `capabilities.json` / `schema.json` / `package.json` / lockfile against the\n * PR's base and fails (`drift_undeclared`) on a high-severity delta outside\n * the declared budget.\n *\n * Scope is deliberately narrow-by-default:\n *\n * - `modules` covers only ADDED actions in the named module (the\n * new-module convenience). Modifying or removing an existing action is\n * never authorized by a module declaration — a mission that expects a\n * guard or signature change on an existing action must name the exact\n * ref in `actions`, so \"add a small feature\" can never quietly license\n * weakening an unrelated action in the same module.\n * - String shorthands (\"issues\" in `tables`, \"issues:createIssue\" in\n * `actions`) mean \"any change\" and are the coarse form; the object forms\n * narrow to specific change kinds.\n *\n * Omitting the block keeps drift report-only, so every existing v1 manifest\n * behaves exactly as before.\n */\nexport interface ExpectedChanges {\n /**\n * Module leaf names (e.g. `\"issues\"`) in which NEW actions may appear.\n * Does not authorize modification or removal of existing actions.\n */\n modules?: readonly string[];\n /** Exact action refs that may change; see {@link ExpectedActionChange}. */\n actions?: readonly (string | ExpectedActionChange)[];\n /** Tables that may change; see {@link ExpectedTableChange}. */\n tables?: readonly (string | ExpectedTableChange)[];\n /**\n * Runtime dependencies (`package.json` `dependencies`) that may be added,\n * removed, or version-changed. Every runtime dependency delta is\n * high-severity — a swap or downgrade changes shipped behavior as surely as\n * an addition.\n */\n dependencies?: readonly string[];\n /**\n * Expect a lockfile-only resolution change (pnpm-lock.yaml differs while\n * the package.json dependency surface does not). Rare and suspicious by\n * default, hence explicitly declared rather than inferred.\n */\n lockfile?: boolean;\n}\n\nexport interface MissionManifest {\n /** Pinned to MISSION_MANIFEST_SCHEMA_VERSION. */\n schemaVersion: number;\n /** Ticket id from the consumer's kanban (e.g. `\"M-042\"`). */\n missionId: string;\n /** Short human-readable title. */\n missionTitle: string;\n /** Product repo slug (informational). */\n productRepo?: string;\n /** PR branch name (informational). */\n prBranch?: string;\n /** ISO-8601 timestamp the manifest was generated. */\n createdAt: string;\n requirements: MissionRequirements;\n /**\n * Optional drift bound; see {@link ExpectedChanges}. Absent means the drift\n * report is informational only.\n */\n expectedChanges?: ExpectedChanges;\n /** Free-form notes for reviewers / the coding agent. */\n acceptanceNotes?: string;\n}\n\n// ---------------------------------------------------------------------------\n// Validation result types\n// ---------------------------------------------------------------------------\n\nexport type ValidationCategory =\n | \"capability_missing\"\n | \"capability_mismatch\"\n | \"schema_missing\"\n | \"schema_column_missing\"\n | \"schema_rls_mismatch\"\n /** A policy grants access the manifest declared must not exist. */\n | \"schema_policy_permits\"\n | \"trace_missing\"\n /**\n * A passing assertion matched the requirement but was recorded directly by\n * spec code (`recordAssertion`) where the requirement demands an SDK-helper\n * origin (`evidence: \"helper\"`, the default for `tenant_isolation`).\n */\n | \"trace_unverified\"\n | \"manifest_shape\"\n /** The Playwright run itself exited non-zero. Reported by the CLI, not the validator. */\n | \"proof_run\"\n /** The run produced no trace artifacts at all, so there is nothing to validate. */\n | \"no_traces\"\n /**\n * A trace in the directory was recorded against a different commit than the\n * current run — leftover evidence that cannot speak for the code under review.\n * Reported by the CLI, which knows the run's commit; the validator sees only\n * the bundle it is handed.\n */\n | \"stale_trace\";\n\n/**\n * Which proof supplied the evidence for a satisfied `trace_must_prove` entry.\n *\n * Recorded for every requirement — satisfied or not — so a reviewer can audit\n * the mapping instead of trusting that \"0 issues\" meant the right proof made\n * the claim. Requirements are matched against all assertions in the run, so\n * without this the attribution would be invisible.\n */\nexport interface RequirementEvidence {\n kind: ProofKind;\n target: string;\n role: AssertionRole;\n satisfied: boolean;\n /** proofId of the trace whose assertion satisfied it, when satisfied. */\n proofId?: string;\n /** Spec file that produced that trace, when known. */\n specFile?: string;\n /** The satisfying assertion's `detail`, verbatim. */\n detail?: string;\n /**\n * The satisfying assertion's helper provenance, when it has one. Absent\n * means the evidence was recorded directly by spec code.\n */\n emittedBy?: string;\n}\n\nexport interface ValidationIssue {\n category: ValidationCategory;\n /** Human-readable summary including expected vs found. */\n message: string;\n /** Structured context for machine consumers (optional). */\n context?: Record<string, unknown>;\n}\n\nexport interface ValidationResult {\n missionId: string;\n ok: boolean;\n issues: ValidationIssue[];\n /** Attribution for every `trace_must_prove` entry, in manifest order. */\n evidence?: RequirementEvidence[];\n}\n","// Import External Packages\n// Import Local Imports\n// Import Core Dependencies\n// Import Shared Dependencies\n// Import Extension Dependencies\n\n// ---------------------------------------------------------------------------\n// Closed Vocabularies\n//\n// These are the ONLY allowed values for verb, kind, and rls_classification\n// across the Proof system. Keeping them closed lets the scanner, manifest\n// validator, and trace consumers all speak the same language.\n// ---------------------------------------------------------------------------\n\n/**\n * Verb — the action a capability performs.\n * Used as metadata in `withProof({ verb: ... })`.\n */\nexport const PROOF_VERBS = [\n \"create\",\n \"read\",\n \"update\",\n \"delete\",\n \"invoke\",\n \"transfer\",\n] as const;\n\nexport type ProofVerb = (typeof PROOF_VERBS)[number];\n\n/**\n * Kind — the category of a trace step / assertion.\n * Used in `trace_must_prove` manifest entries and emitted by `assert.*`\n * helpers and `trace.step({ kind })` calls.\n */\nexport const PROOF_KINDS = [\n \"happy_path\",\n \"tenant_isolation\",\n \"authorization\",\n \"validation\",\n \"error_handling\",\n \"idempotency\",\n] as const;\n\nexport type ProofKind = (typeof PROOF_KINDS)[number];\n\n/**\n * RLS classification — how a table is scoped under Row Level Security.\n * Emitted by the migration parser (v1). Defined here so v0.5 helpers and\n * future tooling share the same vocabulary.\n */\nexport const RLS_CLASSIFICATIONS = [\n \"workspace_scoped\",\n \"user_scoped\",\n \"admin_only\",\n \"public_read\",\n \"service_only\",\n \"unclassified\",\n] as const;\n\nexport type RlsClassification = (typeof RLS_CLASSIFICATIONS)[number];\n\n/**\n * The commands a Postgres RLS policy can cover, as the migration parser reports\n * them.\n *\n * `rls_classification` gives a table one label, which is a useful summary and a\n * poor description: real policies are per-command, so a table can be readable\n * only by admins while being writable only by the service role. `ALL` is Postgres'\n * own wildcard and matches every command, which is why a prohibition on any single\n * command has to consider it.\n */\nexport const POLICY_COMMANDS = [\n \"SELECT\",\n \"INSERT\",\n \"UPDATE\",\n \"DELETE\",\n \"ALL\",\n] as const;\n\nexport type PolicyCommand = (typeof POLICY_COMMANDS)[number];\n\n/**\n * Module kind — which of four very different things a module is.\n *\n * The field a planner reads first, because it decides cost and risk at once.\n * A module moving from `generated` to `extension` is work that will never be\n * paid for twice, and it should be visible in a diff.\n */\nexport const MODULE_KINDS = [\n /** The template ships it. Always present. */\n \"core\",\n /** Installable and already proven. Configured by answering its decisions. */\n \"extension\",\n /** Domain-specific code built for one product, under `__business-logic/`. */\n \"business\",\n /** Does not exist yet. An agent must build it against checks authored first. */\n \"generated\",\n] as const;\n\nexport type ModuleKind = (typeof MODULE_KINDS)[number];\n\n/**\n * How a buyer's answer reaches the code.\n *\n * The only technical field a planner needs, because it is what separates a\n * cheap dispatch from an expensive one. Most of this template is `code`: the\n * survey behind this vocabulary found exactly one real feature flag and about\n * twenty-five decisions that mean deleting routes and components.\n */\nexport const APPLIED_BY = [\n /** Set a value in a named constant. */\n \"config\",\n /** Rewrite prose — legal text, marketing copy. */\n \"content\",\n /** Add or remove files, components, routes, migrations. */\n \"code\",\n] as const;\n\nexport type AppliedBy = (typeof APPLIED_BY)[number];\n\n// ---------------------------------------------------------------------------\n// Type guards\n// ---------------------------------------------------------------------------\n\nexport function isProofVerb(value: unknown): value is ProofVerb {\n return (\n typeof value === \"string\" &&\n (PROOF_VERBS as readonly string[]).includes(value)\n );\n}\n\nexport function isProofKind(value: unknown): value is ProofKind {\n return (\n typeof value === \"string\" &&\n (PROOF_KINDS as readonly string[]).includes(value)\n );\n}\n\nexport function isRlsClassification(\n value: unknown,\n): value is RlsClassification {\n return (\n typeof value === \"string\" &&\n (RLS_CLASSIFICATIONS as readonly string[]).includes(value)\n );\n}\n\nexport function isPolicyCommand(value: unknown): value is PolicyCommand {\n return (\n typeof value === \"string\" &&\n (POLICY_COMMANDS as readonly string[]).includes(value)\n );\n}\n\nexport function isModuleKind(value: unknown): value is ModuleKind {\n return (\n typeof value === \"string\" &&\n (MODULE_KINDS as readonly string[]).includes(value)\n );\n}\n\nexport function isAppliedBy(value: unknown): value is AppliedBy {\n return (\n typeof value === \"string\" &&\n (APPLIED_BY as readonly string[]).includes(value)\n );\n}\n","// Import External Packages\n// Import Local Imports\nimport type { ProofKind } from \"./vocabulary\";\n// Import Core Dependencies\n// Import Shared Dependencies\n// Import Extension Dependencies\n\n// ---------------------------------------------------------------------------\n// Trace artifact types\n//\n// These are the durable shape written to `.proof/traces/<proofId>.json`.\n//\n// Two separate identifiers on purpose:\n// - `proofId` — per-file id; what _this_ proof verifies (e.g. \"auth-login-admin\",\n// \"workspace-invite-tenant-isolation\"). The filename is keyed on it,\n// so proofs never collide.\n// - `missionId` — optional external ticket reference (e.g. \"M-042\"). A single mission\n// typically has multiple proofs (happy_path + tenant_isolation +\n// authorization), each with its own file. The v1 mission-manifest\n// validator groups by `missionId` and checks that every\n// `trace_must_prove` entry has at least one matching passing step\n// across the group.\n// ---------------------------------------------------------------------------\n\n/**\n * The evidential role an assertion plays.\n *\n * - `primary` — the claim the proof exists to make. For an authorization or\n * tenant-isolation proof that is a refusal (\"the outsider could\n * NOT read the row\"); for a happy_path proof it is a success\n * (\"the user reached their workspace\"). This is what a mission's\n * `trace_must_prove` entry matches by default.\n * - `control` — a companion assertion proving the primary probe was capable of\n * failing at all: \"the operation that SHOULD be permitted still\n * succeeded\", or \"the row the primary probe looked for genuinely\n * exists and is readable by someone.\"\n *\n * The distinction is load-bearing, not cosmetic. A refusal probe alone cannot\n * tell \"RLS correctly hid this row\" apart from \"there was no row / the grant\n * denies everyone / the table name was a typo\" — every one of those yields zero\n * rows and would record a passing assertion. The control is what makes the zero\n * mean something.\n *\n * Because the validator matches `trace_must_prove` on `{ kind, target,\n * passed: true }`, controls MUST be labelled: otherwise a control (which by\n * design passes) could satisfy a requirement all on its own and the primary\n * probe would never be checked at all.\n *\n * Naming note: an earlier draft called these `negative` / `positive_control`.\n * That reads backwards for `happy_path`, whose primary claim is a success, so\n * the roles are named by evidential function instead. Both older names are\n * accepted as deprecated aliases wherever a role is parsed.\n */\nexport type AssertionRole = \"primary\" | \"control\";\n\n/**\n * Concrete operation exercised by an assertion. Mutation inventory keys claims\n * by kind + target + operation so a SELECT mutation cannot silently stand in\n * for an untested UPDATE/DELETE or action invocation.\n */\nexport type ProofOperation =\n | \"select\"\n | \"insert\"\n | \"update\"\n | \"delete\"\n | \"invoke\"\n | \"request\";\n\n/** Deprecated role spellings accepted on input and normalized to AssertionRole. */\nexport const ASSERTION_ROLE_ALIASES: Readonly<Record<string, AssertionRole>> = {\n negative: \"primary\",\n positive_control: \"control\",\n};\n\n/**\n * Normalize a role from an artifact or manifest. `undefined` means `primary`:\n * traces written before schemaVersion 2 carry no role, and their assertions\n * were all primary claims.\n */\nexport function normalizeAssertionRole(role: unknown): AssertionRole | null {\n if (role === undefined || role === null) return \"primary\";\n if (role === \"primary\" || role === \"control\") return role;\n if (typeof role === \"string\" && role in ASSERTION_ROLE_ALIASES) {\n return ASSERTION_ROLE_ALIASES[role];\n }\n return null;\n}\n\n/**\n * What an assertion concluded.\n *\n * `passed` / `failed` are the two verdicts a boolean can carry. The other two\n * exist because \"we did not find out\" is a real outcome that a boolean has to\n * round to one side or the other, and rounding it to `false` is indistinguishable\n * from a genuine failure while rounding it to `true` is how a suite quietly stops\n * proving anything:\n *\n * - `incomplete` — the probe ran but could not reach a verdict (a precondition\n * was missing, a dependency was unavailable).\n * - `skipped` — the probe was deliberately not run, e.g. a tenant-isolation\n * direction with no fixture rows on that side.\n *\n * Neither counts as evidence: both carry `passed: false`, so neither can satisfy\n * a mission requirement. The status is what lets a consumer tell them apart from\n * a real failure, and stops a skip from disappearing into \"one fewer assertion\".\n */\nexport type AssertionStatus = \"passed\" | \"failed\" | \"incomplete\" | \"skipped\";\n\n/** Derive the status of an assertion that predates the field. */\nexport function normalizeAssertionStatus(\n status: unknown,\n passed: boolean,\n): AssertionStatus {\n if (\n status === \"passed\" ||\n status === \"failed\" ||\n status === \"incomplete\" ||\n status === \"skipped\"\n ) {\n return status;\n }\n return passed ? \"passed\" : \"failed\";\n}\n\n/**\n * A single structured assertion recorded during a step. Produced by the\n * `assert.*` helpers via `recordAssertion(...)`. Hand-written `expect(...)`\n * calls inside a step do NOT appear here — only helper-driven assertions do,\n * because the v1 mission-manifest validator matches on\n * `{ kind, target, passed: true, role }` and needs a closed, machine-readable\n * surface.\n */\nexport interface TraceAssertion {\n /** Category from the closed `PROOF_KINDS` vocabulary. */\n kind: ProofKind;\n /** The thing asserted against: table, route, action, etc. */\n target: string;\n /** Concrete operation the probe exercised. */\n operation?: ProofOperation;\n /**\n * Whether this specific assertion held. Remains the authoritative field for\n * the validator and for consumers: only `true` is evidence.\n */\n passed: boolean;\n /**\n * Why `passed` has the value it has. Omitted is derived from `passed`, so\n * traces written before this field stay readable.\n */\n status?: AssertionStatus;\n /**\n * Evidential role. Omitted is treated as `\"primary\"` by the validator for\n * backward compatibility with traces written before schemaVersion 2.\n */\n role?: AssertionRole;\n /** Optional human-readable note for failure triage. */\n detail?: string;\n /**\n * Which SDK helper produced this assertion, e.g. `\"assert.tenantIsolation\"`.\n *\n * Stamped by the SDK itself through an internal channel that the public\n * `recordAssertion(...)` entry point cannot reach: a value supplied by spec\n * code is stripped before the assertion is recorded. Absent means the\n * assertion was recorded directly by a spec via `recordAssertion(...)`.\n *\n * The distinction is evidential. A helper-emitted assertion went through the\n * helper's vacuity controls, ground-truth re-reads, and role bookkeeping; a\n * spec-recorded assertion asserts whatever the spec chose to write into it.\n * The mission validator therefore requires helper provenance for\n * `tenant_isolation` requirements (a hand-rolled isolation probe has none of\n * `assert.tenantIsolation`'s protections), and a mission can demand it for\n * any requirement via `trace_must_prove[].evidence: \"helper\"`.\n *\n * For `tenant_isolation` the stamp is also identity-checked, not merely\n * present-checked — see TENANT_ISOLATION_TABLE_HELPERS. `assert.httpResponse`\n * records whatever `kind` its caller passes, so \"some helper emitted this\"\n * is not the same claim as \"the isolation probe ran\".\n */\n emittedBy?: string;\n}\n\n/**\n * The only helpers whose stamp counts as tenant-isolation evidence, split by\n * target (tables are stricter than action refs). The single runtime source is\n * [`portable-vocabulary.mjs`](./portable-vocabulary.mjs), which the\n * dependency-free Node scripts (`proof_verify.mjs`, `proof_coverage.mjs`)\n * import directly; see there for the identity rationale.\n */\nexport {\n TENANT_ISOLATION_ACTION_HELPERS,\n TENANT_ISOLATION_TABLE_HELPERS,\n} from \"./portable-vocabulary.mjs\";\n\n/**\n * A single step inside a proof.\n * Produced by `t.step({ intent, kind, target }, async () => { ... })`.\n */\nexport interface TraceStep {\n /** Human-readable description of what the step attempted. */\n intent: string;\n /** Category of the step, from closed vocabulary. */\n kind: ProofKind;\n /** The thing the step targets: action name, table, route, etc. */\n target: string;\n /** What the step observed (returned by the step callback). */\n observation: string;\n /** Whether the step's assertions held. */\n passed: boolean;\n /** Optional: who the step acted as (email, role, seed identifier). */\n actor?: string;\n /** Optional: which workspace the step operated in. */\n workspaceId?: string;\n /** Wall-clock duration of the step callback. */\n durationMs: number;\n /** On failure, the error message captured. */\n error?: string;\n /**\n * Structured assertions recorded by `assert.*` helpers during this step.\n * Empty or omitted for steps that only use `expect(...)` directly.\n * The v1 mission-manifest validator matches `trace_must_prove` entries\n * against this array.\n */\n assertions?: TraceAssertion[];\n}\n\n/**\n * Current per-proof trace artifact schema version.\n *\n * 2 — adds `role` and `status` to assertions and the `schemaVersion` /\n * `specFile` / `specHash` / `commit` / `dirty` provenance fields below.\n * Consumers written against v1 keep working: every v1 field is still present\n * with the same meaning, and both new assertion fields are derivable when\n * absent.\n *\n * `commit`, `dirty`, and `mutation` arrived after v2 shipped and did not bump\n * it: they are optional additions, and the contract's rule is that additive\n * fields do not move a version. A consumer that needs them must treat absence\n * as \"unknown\" anyway, since normal runs have no mutation and a non-git\n * checkout cannot supply code provenance.\n */\nexport const TRACE_ARTIFACT_SCHEMA_VERSION = 2;\n\n/**\n * Provenance attached only when the mutation harness deliberately plants a\n * defect. A red mutation trace means the check detected the named defect; it\n * must never be mistaken for a real application regression.\n */\nexport interface TraceMutation {\n /** Stable mutation identifier from the repository-owned mutation catalog. */\n id: string;\n /** Always true; makes the intentional nature explicit to copied consumers. */\n planted: true;\n}\n\n/**\n * A trace artifact — one proof's worth of evidence.\n * Written atomically to `.proof/traces/<proofId>.json` when the proof\n * completes (pass or fail).\n */\nexport interface TraceArtifact {\n /**\n * Pinned to TRACE_ARTIFACT_SCHEMA_VERSION. Absent on traces written by\n * SDK versions before provenance was added.\n */\n schemaVersion?: number;\n /**\n * Repo-relative path of the spec file that produced this trace, e.g.\n * `e2e/proofs/workspace-invite.proof.ts`. Lets a reviewer go from a claim\n * straight to the code that made it.\n */\n specFile?: string;\n /**\n * First 12 hex chars of the SHA-256 of the spec file's contents. Two runs\n * claiming the same proof with different hashes were produced by different\n * code — which is the whole question when a previously-red proof turns\n * green.\n */\n specHash?: string;\n /**\n * Full SHA of the commit that was checked out while the proof ran. `specHash`\n * pins the spec that made the claim; this pins the application it was made\n * against, which is what lets an external ledger say \"red at X, green at Y\".\n *\n * Absent outside a git checkout. On a CI `pull_request` run this is the\n * ephemeral merge commit the checkout created — accurate about what was\n * tested, but not a commit you can return to later.\n */\n commit?: string;\n /**\n * Whether the working tree had uncommitted changes when the proof ran. A pass\n * from a dirty tree is evidence about code that exists nowhere but one laptop,\n * so a consumer gating on traces should be able to refuse it. Absent when it\n * could not be determined, which is not the same as `false`.\n */\n dirty?: boolean;\n /**\n * Present only for intentional red runs produced by `proof:mutate`.\n * Stored inside the trace so provenance survives artifact renames/copies.\n */\n mutation?: TraceMutation;\n /**\n * Per-file identifier for this proof. Used as the filename stem and as the\n * join key when the v1 mission-manifest validator looks for a matching\n * passing step for a `trace_must_prove` entry.\n */\n proofId: string;\n /**\n * Optional reference to the external mission (ticket) this proof belongs\n * to. Multiple proofs share one `missionId`. Absent in v0.5 local runs\n * that are not tied to a mission.\n */\n missionId?: string;\n /** ISO 8601 timestamp of proof start. */\n timestamp: string;\n /** Total wall-clock duration across all steps. */\n durationMs: number;\n /** True only if every step passed. */\n passed: boolean;\n /** Ordered list of steps as they were executed. */\n steps: TraceStep[];\n}\n\n/**\n * Options accepted by `trace.proof(...)`. A bare string is treated as\n * `{ proofId: string }` — the common v0.5 case where no mission is attached.\n */\nexport interface ProofOptions {\n proofId: string;\n missionId?: string;\n}\n\n/**\n * The handle passed to a proof callback. Provides the step recorder.\n */\nexport interface TraceRecorder {\n step<T extends StepResult | void>(\n opts: StepOptions,\n fn: () => Promise<T>,\n ): Promise<T>;\n}\n\nexport interface StepOptions {\n intent: string;\n kind: ProofKind;\n target: string;\n actor?: string;\n workspaceId?: string;\n}\n\n/**\n * Optional payload returned by a step callback. If the callback returns\n * `void` the step is still recorded with `observation: \"\"`.\n */\nexport interface StepResult {\n observation?: string;\n}\n","// ---------------------------------------------------------------------------\n// Mission manifest validator.\n//\n// Three-way cross-reference:\n// 1. Manifest is shape-correct (schemaVersion + required fields).\n// 2. Every `capabilities_must_exist[i]` is found in `.proof/capabilities.json`.\n// 3. Every `schema_must_contain[i]` is found in `.proof/schema.json` with\n// matching required_columns and rls_classification.\n// 4. No policy in `.proof/schema.json` grants what a\n// `policies_must_not_allow[i]` entry forbids. This is the per-command view\n// that `rls_classification`'s single label per table cannot express.\n// 5. Every `trace_must_prove[i]` has at least one passing assertion in the\n// aggregated trace with matching `kind` + `target` + `role` (default\n// `primary`), optionally pinned to a specific `proofId`. Which proof\n// supplied each piece of evidence is reported back in `evidence` so the\n// attribution is auditable rather than implicit.\n//\n// Produces a structured ValidationResult; the CLI driver is responsible for\n// formatting to `[PROOF_FAIL]` lines and exit codes.\n//\n// Intentionally zero runtime dependencies — this file runs under Node during\n// `pnpm proof:verify` and inside CI, and must stay fast-starting.\n// ---------------------------------------------------------------------------\n\nimport {\n ACTION_CHANGE_KINDS,\n MISSION_MANIFEST_SCHEMA_VERSION,\n requiredEvidenceOrigin,\n TABLE_CHANGE_KINDS,\n type MissionManifest,\n type RequirementEvidence,\n type ValidationIssue,\n type ValidationResult,\n} from \"../shared/mission-types\";\nimport {\n isPolicyCommand,\n isProofKind,\n isProofVerb,\n isRlsClassification,\n} from \"../shared/vocabulary\";\nimport {\n normalizeAssertionRole,\n TENANT_ISOLATION_ACTION_HELPERS,\n TENANT_ISOLATION_TABLE_HELPERS,\n type TraceArtifact,\n type TraceAssertion,\n} from \"../shared/trace-types\";\n\n// ---------------------------------------------------------------------------\n// External artifact shapes (only the fields we need)\n// ---------------------------------------------------------------------------\n\nexport interface CapabilitiesArtifact {\n schemaVersion: number;\n capabilities: Array<{\n name: string;\n module: string;\n /** Literal exported wrapper function used by the generated invoke registry. */\n exportName?: string | null;\n verb: string | null;\n object: string | null;\n invariants: readonly string[];\n /** Derived scanner signal; absent on older additive v1 artifacts. */\n acceptsWorkspaceId?: boolean;\n /** Internal BOT/server-only plumbing is not a user-invoked boundary. */\n internalOnly?: boolean;\n /** Direct next/cache updateTag is not safe through the proof route. */\n usesDirectUpdateTag?: boolean;\n /** Concrete writes issued through createSupabaseServiceClient(). */\n serviceRoleMutations?: ReadonlyArray<{\n table: string;\n operation: \"insert\" | \"upsert\" | \"update\" | \"delete\";\n }>;\n /** Relevant guards observed in the action pipeline. */\n middleware?: {\n auth: boolean;\n tenantIsolation: boolean;\n rbac: boolean;\n };\n file: string;\n }>;\n unclassified?: readonly unknown[];\n}\n\nexport interface SchemaPolicy {\n name: string;\n /** One of POLICY_COMMANDS as the parser reports it. */\n command: string;\n /** Roles from the policy's `TO` clause. Empty means no clause, i.e. PUBLIC. */\n roles: readonly string[];\n}\n\nexport interface SchemaArtifact {\n schemaVersion: number;\n tables: Array<{\n name: string;\n columns: readonly string[];\n rls_classification: string;\n policies: readonly SchemaPolicy[];\n sourceFiles: readonly string[];\n }>;\n unclassified?: readonly unknown[];\n}\n\n/**\n * A trace bundle for validation is the aggregated list of every trace the\n * proof run emitted. Kept as `TraceArtifact[]` so v0.6's per-proof JSON\n * files can be concatenated without transformation.\n */\nexport type TraceBundle = readonly TraceArtifact[];\n\n// ---------------------------------------------------------------------------\n// Shape validation\n// ---------------------------------------------------------------------------\n\nfunction isNonEmptyString(value: unknown): value is string {\n return typeof value === \"string\" && value.length > 0;\n}\n\n/**\n * Validate the optional `expectedChanges` block. Kept separate because the\n * shape has real structure (string shorthands vs narrowing objects) and a\n * malformed budget must fail loudly rather than half-enforce.\n */\nfunction expectedChangesShapeIssues(block: unknown): ValidationIssue[] {\n const issues: ValidationIssue[] = [];\n if (block === undefined) return issues;\n if (!block || typeof block !== \"object\" || Array.isArray(block)) {\n issues.push({\n category: \"manifest_shape\",\n message: \"expectedChanges must be an object when present\",\n });\n return issues;\n }\n const changes = block as Record<string, unknown>;\n\n for (const key of [\"modules\", \"dependencies\"] as const) {\n const list = changes[key];\n if (list === undefined) continue;\n if (!Array.isArray(list) || !list.every(isNonEmptyString)) {\n issues.push({\n category: \"manifest_shape\",\n message: `expectedChanges.${key} must be an array of non-empty strings when present`,\n });\n }\n }\n\n const validateEntryList = (\n key: \"actions\" | \"tables\",\n nameField: \"ref\" | \"name\",\n kinds: readonly string[],\n ) => {\n const list = changes[key];\n if (list === undefined) return;\n if (!Array.isArray(list)) {\n issues.push({\n category: \"manifest_shape\",\n message: `expectedChanges.${key} must be an array when present`,\n });\n return;\n }\n for (const entry of list) {\n if (isNonEmptyString(entry)) continue;\n if (!entry || typeof entry !== \"object\" || Array.isArray(entry)) {\n issues.push({\n category: \"manifest_shape\",\n message: `expectedChanges.${key} entries must be non-empty strings or { ${nameField}, changes? } objects`,\n });\n continue;\n }\n const record = entry as Record<string, unknown>;\n if (!isNonEmptyString(record[nameField])) {\n issues.push({\n category: \"manifest_shape\",\n message: `expectedChanges.${key}[].${nameField} is required (non-empty string)`,\n });\n }\n if (record.changes !== undefined) {\n if (\n !Array.isArray(record.changes) ||\n record.changes.length === 0 ||\n !record.changes.every((c) => kinds.includes(c as string))\n ) {\n issues.push({\n category: \"manifest_shape\",\n message: `expectedChanges.${key}[].changes must be a non-empty array drawn from: ${kinds.join(\", \")}`,\n });\n }\n }\n }\n };\n validateEntryList(\"actions\", \"ref\", ACTION_CHANGE_KINDS);\n validateEntryList(\"tables\", \"name\", TABLE_CHANGE_KINDS);\n\n if (changes.lockfile !== undefined && typeof changes.lockfile !== \"boolean\") {\n issues.push({\n category: \"manifest_shape\",\n message: \"expectedChanges.lockfile must be a boolean when present\",\n });\n }\n\n return issues;\n}\n\nfunction shapeIssues(manifest: unknown): ValidationIssue[] {\n const issues: ValidationIssue[] = [];\n if (!manifest || typeof manifest !== \"object\") {\n issues.push({\n category: \"manifest_shape\",\n message: \"manifest is not a JSON object\",\n });\n return issues;\n }\n const m = manifest as Partial<MissionManifest>;\n\n if (m.schemaVersion !== MISSION_MANIFEST_SCHEMA_VERSION) {\n issues.push({\n category: \"manifest_shape\",\n message: `expected schemaVersion=${MISSION_MANIFEST_SCHEMA_VERSION}, found ${String(m.schemaVersion)}`,\n });\n }\n if (typeof m.missionId !== \"string\" || m.missionId.length === 0) {\n issues.push({\n category: \"manifest_shape\",\n message: \"missionId is required (non-empty string)\",\n });\n }\n if (typeof m.missionTitle !== \"string\" || m.missionTitle.length === 0) {\n issues.push({\n category: \"manifest_shape\",\n message: \"missionTitle is required (non-empty string)\",\n });\n }\n if (typeof m.createdAt !== \"string\" || m.createdAt.length === 0) {\n issues.push({\n category: \"manifest_shape\",\n message: \"createdAt is required (ISO-8601 string)\",\n });\n }\n\n // Optional drift bound: absent is fine, present-but-wrong is not — a typo'd\n // scope list would otherwise silently disable the enforcement it names.\n issues.push(...expectedChangesShapeIssues(m.expectedChanges));\n\n const req = m.requirements;\n if (!req || typeof req !== \"object\") {\n issues.push({\n category: \"manifest_shape\",\n message: \"requirements block is required\",\n });\n return issues;\n }\n if (!Array.isArray(req.capabilities_must_exist)) {\n issues.push({\n category: \"manifest_shape\",\n message: \"requirements.capabilities_must_exist must be an array\",\n });\n }\n if (!Array.isArray(req.schema_must_contain)) {\n issues.push({\n category: \"manifest_shape\",\n message: \"requirements.schema_must_contain must be an array\",\n });\n }\n if (!Array.isArray(req.trace_must_prove)) {\n issues.push({\n category: \"manifest_shape\",\n message: \"requirements.trace_must_prove must be an array\",\n });\n }\n // Optional block: absent is fine, present-but-wrong is not.\n if (\n req.policies_must_not_allow !== undefined &&\n !Array.isArray(req.policies_must_not_allow)\n ) {\n issues.push({\n category: \"manifest_shape\",\n message:\n \"requirements.policies_must_not_allow must be an array when present\",\n });\n }\n\n // Vocabulary checks (best-effort — don't fail if fields are missing since\n // shape errors above will already have been reported).\n for (const cap of req.capabilities_must_exist ?? []) {\n if (!isProofVerb((cap as { verb?: unknown }).verb)) {\n issues.push({\n category: \"manifest_shape\",\n message: `capabilities_must_exist[].verb \"${String((cap as { verb?: unknown }).verb)}\" is not in PROOF_VERBS`,\n });\n }\n }\n for (const s of req.schema_must_contain ?? []) {\n if (\n !isRlsClassification(\n (s as { rls_classification?: unknown }).rls_classification,\n )\n ) {\n issues.push({\n category: \"manifest_shape\",\n message: `schema_must_contain[].rls_classification \"${String((s as { rls_classification?: unknown }).rls_classification)}\" is not in RLS_CLASSIFICATIONS`,\n });\n }\n }\n for (const t of req.trace_must_prove ?? []) {\n if (!isProofKind((t as { kind?: unknown }).kind)) {\n issues.push({\n category: \"manifest_shape\",\n message: `trace_must_prove[].kind \"${String((t as { kind?: unknown }).kind)}\" is not in PROOF_KINDS`,\n });\n }\n const evidence = (t as { evidence?: unknown }).evidence;\n if (evidence !== undefined && evidence !== \"helper\" && evidence !== \"any\") {\n issues.push({\n category: \"manifest_shape\",\n message: `trace_must_prove[].evidence \"${String(evidence)}\" is not a valid evidence origin; expected \"helper\" or \"any\"`,\n });\n }\n // Tenant isolation admits no opt-out: strict coverage rejects raw\n // isolation assertions unconditionally, and the mission gate must never\n // accept what coverage refuses. Bespoke escapes live in mutation policy,\n // not here.\n if (\n evidence === \"any\" &&\n (t as { kind?: unknown }).kind === \"tenant_isolation\"\n ) {\n issues.push({\n category: \"manifest_shape\",\n message:\n 'trace_must_prove[].evidence \"any\" is not allowed for kind \"tenant_isolation\"; isolation evidence must come from an SDK helper (assert.tenantIsolation, or assert.authorization for action targets)',\n });\n }\n }\n for (const p of Array.isArray(req.policies_must_not_allow)\n ? req.policies_must_not_allow\n : []) {\n const prohibition = p as { role?: unknown; commands?: unknown };\n if (typeof prohibition.role !== \"string\" || prohibition.role.length === 0) {\n issues.push({\n category: \"manifest_shape\",\n message:\n \"policies_must_not_allow[].role is required (non-empty database role name)\",\n });\n }\n // A typo'd command would otherwise match nothing and pass, turning a\n // security claim into a comment.\n if (prohibition.commands !== undefined) {\n if (!Array.isArray(prohibition.commands)) {\n issues.push({\n category: \"manifest_shape\",\n message:\n \"policies_must_not_allow[].commands must be an array when present\",\n });\n } else {\n for (const command of prohibition.commands) {\n if (!isPolicyCommand(command)) {\n issues.push({\n category: \"manifest_shape\",\n message: `policies_must_not_allow[].commands entry \"${String(command)}\" is not in POLICY_COMMANDS`,\n });\n }\n }\n }\n }\n }\n\n return issues;\n}\n\n// ---------------------------------------------------------------------------\n// Cross-reference checks\n// ---------------------------------------------------------------------------\n\nfunction checkCapabilities(\n manifest: MissionManifest,\n capabilities: CapabilitiesArtifact,\n): ValidationIssue[] {\n const issues: ValidationIssue[] = [];\n for (const req of manifest.requirements.capabilities_must_exist) {\n const match = capabilities.capabilities.find(\n (c) => c.name === req.name && c.module === req.module,\n );\n if (!match) {\n issues.push({\n category: \"capability_missing\",\n message: `expected capability ${req.module}:${req.name} (verb=${req.verb}, object=${req.object}); not found in capabilities.json`,\n context: { requirement: req },\n });\n continue;\n }\n if (match.verb !== req.verb) {\n issues.push({\n category: \"capability_mismatch\",\n message: `capability ${req.module}:${req.name} has verb=\"${match.verb ?? \"<missing>\"}\" but manifest requires \"${req.verb}\"; add or update withProof({ verb }) on the action`,\n context: { requirement: req, found: match },\n });\n }\n if (req.object && match.object !== req.object) {\n issues.push({\n category: \"capability_mismatch\",\n message: `capability ${req.module}:${req.name} has object=\"${match.object ?? \"<missing>\"}\" but manifest requires \"${req.object}\"; add or update withProof({ object }) on the action`,\n context: { requirement: req, found: match },\n });\n }\n if (req.invariants && req.invariants.length > 0) {\n const missing = req.invariants.filter(\n (inv) => !match.invariants.includes(inv),\n );\n if (missing.length > 0) {\n issues.push({\n category: \"capability_mismatch\",\n message: `capability ${req.module}:${req.name} is missing invariants: ${missing.join(\", \")}; add them to withProof({ invariants })`,\n context: { requirement: req, found: match, missing },\n });\n }\n }\n }\n return issues;\n}\n\nfunction checkSchema(\n manifest: MissionManifest,\n schema: SchemaArtifact,\n): ValidationIssue[] {\n const issues: ValidationIssue[] = [];\n for (const req of manifest.requirements.schema_must_contain) {\n const table = schema.tables.find((t) => t.name === req.table);\n if (!table) {\n issues.push({\n category: \"schema_missing\",\n message: `expected table \"${req.table}\" not found in schema.json; add a CREATE TABLE migration`,\n context: { requirement: req },\n });\n continue;\n }\n const missingCols = req.required_columns.filter(\n (c) => !table.columns.includes(c),\n );\n if (missingCols.length > 0) {\n issues.push({\n category: \"schema_column_missing\",\n message: `table \"${req.table}\" is missing columns: ${missingCols.join(\", \")}; found columns: ${table.columns.join(\", \")}`,\n context: {\n requirement: req,\n missing: missingCols,\n found: table.columns,\n },\n });\n }\n if (table.rls_classification !== req.rls_classification) {\n issues.push({\n category: \"schema_rls_mismatch\",\n message: `table \"${req.table}\" has rls_classification=\"${table.rls_classification}\" but manifest requires \"${req.rls_classification}\"; review RLS policies on the table`,\n context: { requirement: req, found: table.rls_classification },\n });\n }\n }\n return issues;\n}\n\n/**\n * Whether a policy hands `role` any authority.\n *\n * An empty `roles` list is Postgres' `PUBLIC` default — a policy written without\n * a `TO` clause applies to everyone, so treating \"no roles listed\" as \"no roles\n * granted\" would read the most permissive policy as the most restrictive one.\n */\nfunction policyGrantsRole(policy: SchemaPolicy, role: string): boolean {\n if (policy.roles.length === 0) return true;\n return policy.roles.some(\n (r) => r === role || r === \"public\" || r === \"PUBLIC\",\n );\n}\n\n/** Whether a policy's command is covered by a prohibition's command list. */\nfunction policyMatchesCommands(\n policy: SchemaPolicy,\n commands: readonly string[] | undefined,\n): boolean {\n if (!commands || commands.length === 0) return true;\n // `FOR ALL` covers every command, so it matches whatever was asked about.\n if (policy.command === \"ALL\") return true;\n return commands.includes(policy.command);\n}\n\nfunction checkPolicies(\n manifest: MissionManifest,\n schema: SchemaArtifact,\n): ValidationIssue[] {\n const issues: ValidationIssue[] = [];\n for (const req of manifest.requirements.policies_must_not_allow ?? []) {\n const table = schema.tables.find((t) => t.name === req.table);\n if (!table) {\n issues.push({\n category: \"schema_missing\",\n message: `expected table \"${req.table}\" not found in schema.json; add a CREATE TABLE migration`,\n context: { requirement: req },\n });\n continue;\n }\n const offending = table.policies.filter(\n (p) =>\n policyGrantsRole(p, req.role) && policyMatchesCommands(p, req.commands),\n );\n if (offending.length > 0) {\n const scope = req.commands?.length\n ? req.commands.join(\", \")\n : \"any command\";\n issues.push({\n category: \"schema_policy_permits\",\n message:\n `table \"${req.table}\" must not allow role \"${req.role}\" (${scope}), ` +\n `but ${offending.length} policy/policies do: ` +\n offending\n .map(\n (p) =>\n `\"${p.name}\" FOR ${p.command} TO ${p.roles.length > 0 ? p.roles.join(\"/\") : \"PUBLIC (no TO clause)\"}`,\n )\n .join(\", \") +\n `; requirement: \"${req.description}\"`,\n context: { requirement: req, offending },\n });\n }\n }\n return issues;\n}\n\n/**\n * An assertion plus the proof it came from. Attribution has to survive the\n * flattening, otherwise a satisfied requirement can't be traced back to the\n * spec that satisfied it.\n */\nexport interface AttributedAssertion extends TraceAssertion {\n proofId: string;\n specFile?: string;\n}\n\n/** Flatten every assertion across every trace / step, keeping attribution. */\nexport function collectAssertions(bundle: TraceBundle): AttributedAssertion[] {\n const out: AttributedAssertion[] = [];\n for (const trace of bundle) {\n for (const step of trace.steps ?? []) {\n for (const a of step.assertions ?? []) {\n out.push({\n ...a,\n proofId: trace.proofId,\n ...(trace.specFile ? { specFile: trace.specFile } : {}),\n });\n }\n }\n }\n return out;\n}\n\n/**\n * Which helper stamps may satisfy a requirement, or `null` for \"any stamp\".\n *\n * Only `tenant_isolation` is identity-bound: `assert.httpResponse` records\n * whatever `kind` its caller passes, so a mere presence check would let an\n * HTTP probe labelled `tenant_isolation` count as isolation evidence. Table\n * targets accept only `assert.tenantIsolation` (the dual-tenant probe);\n * action targets also accept `assert.authorization`, whose action probes\n * record the cross-tenant invariant they establish.\n */\nfunction allowedHelpersFor(\n kind: string,\n target: string,\n tableNames: ReadonlySet<string>,\n): readonly string[] | null {\n if (kind !== \"tenant_isolation\") return null;\n const head = String(target).split(\".\")[0];\n return tableNames.has(head)\n ? TENANT_ISOLATION_TABLE_HELPERS\n : TENANT_ISOLATION_ACTION_HELPERS;\n}\n\nfunction checkTraces(\n manifest: MissionManifest,\n bundle: TraceBundle,\n schema: SchemaArtifact,\n): { issues: ValidationIssue[]; evidence: RequirementEvidence[] } {\n const issues: ValidationIssue[] = [];\n const evidence: RequirementEvidence[] = [];\n const assertions = collectAssertions(bundle);\n const tableNames = new Set((schema.tables ?? []).map((t) => t.name));\n\n for (const req of manifest.requirements.trace_must_prove) {\n const requiredRole = normalizeAssertionRole(req.role);\n if (requiredRole === null) {\n issues.push({\n category: \"manifest_shape\",\n message: `trace_must_prove[].role \"${String(req.role)}\" is not a valid assertion role; expected \"primary\" or \"control\"`,\n context: { requirement: req },\n });\n continue;\n }\n\n // Candidates share kind + target (and proofId when the requirement pins\n // one). Everything after this is about explaining *why* a candidate does\n // or does not count as evidence.\n const candidates = assertions.filter(\n (a) =>\n a.kind === req.kind &&\n a.target === req.target &&\n (req.proofId === undefined || a.proofId === req.proofId),\n );\n const requiredOrigin = requiredEvidenceOrigin(req.kind, req.evidence);\n const allowedHelpers = allowedHelpersFor(req.kind, req.target, tableNames);\n const originSatisfied = (a: TraceAssertion) =>\n requiredOrigin === \"any\" ||\n (typeof a.emittedBy === \"string\" &&\n (allowedHelpers === null || allowedHelpers.includes(a.emittedBy)));\n const match = candidates.find(\n (a) =>\n a.passed === true &&\n normalizeAssertionRole(a.role) === requiredRole &&\n originSatisfied(a),\n );\n\n if (match) {\n evidence.push({\n kind: req.kind,\n target: req.target,\n role: requiredRole,\n satisfied: true,\n proofId: match.proofId,\n ...(match.specFile ? { specFile: match.specFile } : {}),\n ...(match.detail ? { detail: match.detail } : {}),\n ...(match.emittedBy ? { emittedBy: match.emittedBy } : {}),\n });\n continue;\n }\n\n evidence.push({\n kind: req.kind,\n target: req.target,\n role: requiredRole,\n satisfied: false,\n });\n\n // Explain the near miss. \"No passing assertion found\" sends people hunting\n // for a missing proof when the real cause is usually one of these three.\n const failed = candidates.filter((a) => a.passed === false);\n const wrongRole = candidates.filter(\n (a) =>\n a.passed === true && normalizeAssertionRole(a.role) !== requiredRole,\n );\n const unverified = candidates.filter(\n (a) =>\n a.passed === true &&\n normalizeAssertionRole(a.role) === requiredRole &&\n !originSatisfied(a),\n );\n\n // A passing right-role assertion exists but its origin disqualifies it —\n // spec-recorded where a helper is demanded, or stamped by a helper that\n // cannot vouch for this kind (assert.httpResponse records the caller's\n // kind verbatim). Its own category: the fix is different and the failure\n // must not read as \"no proof was written\".\n if (unverified.length > 0) {\n const found = unverified[0].emittedBy\n ? `was emitted by ${unverified[0].emittedBy}, which cannot vouch for this claim`\n : \"was recorded directly by spec code (no emittedBy)\";\n const wanted = allowedHelpers\n ? allowedHelpers.join(\" or \")\n : \"an SDK assert.* helper\";\n issues.push({\n category: \"trace_unverified\",\n message:\n `a passing role=\"${requiredRole}\" assertion with kind=\"${req.kind}\" and target=\"${req.target}\" exists but ${found}, ` +\n `and this requirement demands evidence=\"helper\"${req.evidence === undefined ? ` (mandatory for ${req.kind})` : \"\"}. ` +\n `Emit the claim through ${wanted}` +\n `${allowedHelpers === null ? `, or set evidence: \"any\" on the requirement if a bespoke probe is intended` : \"\"}; description: \"${req.description}\"`,\n context: { requirement: req, unverifiedCount: unverified.length },\n });\n continue;\n }\n\n let reason: string;\n if (failed.length > 0) {\n reason =\n `found ${failed.length} matching assertion(s) but they FAILED: ` +\n failed\n .map((a) => `[${a.proofId}] ${a.detail ?? \"(no detail)\"}`)\n .join(\" | \");\n } else if (wrongRole.length > 0) {\n reason =\n `found ${wrongRole.length} passing assertion(s) with role=\"${normalizeAssertionRole(wrongRole[0].role)}\" but this requirement needs role=\"${requiredRole}\". ` +\n `A \"control\" assertion passes by design and only shows the probe could have failed — it cannot stand in for the claim itself. ` +\n `Add the missing ${requiredRole} probe`;\n } else if (req.proofId !== undefined) {\n reason = `no assertion with this kind+target was emitted by proof \"${req.proofId}\" (other proofs may have emitted one; this requirement is pinned)`;\n } else {\n reason = \"no assertion with this kind+target was emitted by any proof\";\n }\n\n issues.push({\n category: \"trace_missing\",\n message: `no passing role=\"${requiredRole}\" assertion found with kind=\"${req.kind}\" and target=\"${req.target}\"; ${reason}; description: \"${req.description}\"`,\n context: { requirement: req, candidateCount: candidates.length },\n });\n }\n\n return { issues, evidence };\n}\n\n// ---------------------------------------------------------------------------\n// Entry point\n// ---------------------------------------------------------------------------\n\nexport interface ValidateMissionInput {\n manifest: unknown;\n capabilities: CapabilitiesArtifact;\n schema: SchemaArtifact;\n traces: TraceBundle;\n}\n\nexport function validateMission(input: ValidateMissionInput): ValidationResult {\n const shape = shapeIssues(input.manifest);\n if (shape.length > 0) {\n return {\n missionId:\n (input.manifest as { missionId?: string } | null)?.missionId ??\n \"<invalid>\",\n ok: false,\n issues: shape,\n };\n }\n\n const manifest = input.manifest as MissionManifest;\n const traceResult = checkTraces(manifest, input.traces, input.schema);\n const issues: ValidationIssue[] = [\n ...checkCapabilities(manifest, input.capabilities),\n ...checkSchema(manifest, input.schema),\n ...checkPolicies(manifest, input.schema),\n ...traceResult.issues,\n ];\n\n return {\n missionId: manifest.missionId,\n ok: issues.length === 0,\n issues,\n evidence: traceResult.evidence,\n };\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,433 @@
1
+ import { Page, APIResponse } from '@playwright/test';
2
+ import { SupabaseClient } from '@supabase/supabase-js';
3
+ import { A as ActionResult } from './action-types-C3OmHRyH.js';
4
+ import { P as ProofFixtureContext, a as ProofFixtureFactory } from './fixture-B-dmnG5G.js';
5
+ import { h as ProofKind, d as AssertionRole, a as TraceAssertion, j as ProofOptions, q as TraceStep, m as StepResult, S as StepOptions } from './trace-types-DkIzq-l9.js';
6
+
7
+ declare const actAsUser: {
8
+ /**
9
+ * Log in as one of the seeded development users (`admin` or `member`).
10
+ * The consumer application's proof login route resolves the role to its
11
+ * configured server-only seed credentials.
12
+ */
13
+ login(page: Page, role: "admin" | "member"): Promise<void>;
14
+ /**
15
+ * Log in as an arbitrary user — typically one created by `seed.user()`
16
+ * inside a `.proof.ts` spec. Used by `assert.tenantIsolation` and any
17
+ * spec that creates fresh users per run.
18
+ */
19
+ loginAs(page: Page, email: string, password: string): Promise<void>;
20
+ /**
21
+ * Invoke a server action end-to-end while authenticated as the given user.
22
+ *
23
+ * Unlike `actAsUser.supabaseClient` — which goes directly to the DB and
24
+ * therefore only proves RLS — this helper goes through the full Next.js
25
+ * request lifecycle: route handler → middleware pipeline → `withAuth`,
26
+ * `withTenantIsolation`, the action's own logic, and any Zod validation.
27
+ * It is the primitive you reach for when proving action-layer invariants
28
+ * (RBAC rejection, validation errors, business-logic branching) that RLS
29
+ * alone cannot cover.
30
+ *
31
+ * The action must be listed in
32
+ * the application's generated Proof action registry. Unregistered actions
33
+ * return a 404 with a structured [PROOF_FAIL] message.
34
+ *
35
+ * Authentication:
36
+ * - `as: "admin" | "member"` — uses the seeded dev users
37
+ * - `as: { email, password }` — uses arbitrary credentials (typically a
38
+ * user created by `seed.user()` earlier in the proof)
39
+ *
40
+ * A registered action returns an `ActionResult<TData>`. Harness failures
41
+ * (missing registry entry, malformed request, action escaping the pipeline)
42
+ * throw instead: absence or breakage of the thing under test must never
43
+ * satisfy a refusal proof.
44
+ *
45
+ * @example
46
+ * const result = await actAsUser.invokeAction<{ removed: boolean }>(
47
+ * page,
48
+ * {
49
+ * as: "member",
50
+ * module: "workspace",
51
+ * action: "removeMember",
52
+ * inputParams: { formData: { workspaceId, memberId } },
53
+ * },
54
+ * );
55
+ * expect(result.success, "[PROOF_FAIL] authorization: ...").toBe(false);
56
+ */
57
+ invokeAction<TData = unknown>(page: Page, opts: {
58
+ as: "admin" | "member" | {
59
+ email: string;
60
+ password: string;
61
+ };
62
+ module: string;
63
+ action: string;
64
+ inputParams: Record<string, unknown>;
65
+ }): Promise<ActionResult<TData>>;
66
+ /**
67
+ * Return a Supabase client authenticated as the given user via
68
+ * `signInWithPassword` against a *fresh* client (no cookies, no session
69
+ * persistence). Queries made through the returned client run with that
70
+ * user's RLS privileges.
71
+ *
72
+ * Use for server-side proofs where the browser is irrelevant and you want
73
+ * direct evidence of whether the DB allows or denies a specific operation
74
+ * for a specific user. This is the primitive behind the primary check in
75
+ * `assert.tenantIsolation` and is intentionally exposed for specs that
76
+ * need finer-grained RLS probes.
77
+ */
78
+ supabaseClient(opts: {
79
+ email: string;
80
+ password: string;
81
+ }): Promise<SupabaseClient>;
82
+ };
83
+
84
+ interface TenantIsolationBaseOptions {
85
+ /**
86
+ * Table to query as userB. RLS on the table is what is being tested.
87
+ * Must be a table governed by a tenant-scoped RLS policy — either
88
+ * workspace-scoped (the canonical pattern uses `get_user_workspace_ids`) or
89
+ * user-scoped (`user_id = auth.uid()`); see `scope`.
90
+ */
91
+ table: string;
92
+ /**
93
+ * What "belongs to one tenant" means for this table:
94
+ *
95
+ * - `"workspace"` (default) — rows carry a workspace id, and the two tenants
96
+ * are the seeded orgA / orgB.
97
+ * - `"user"` — rows carry a user id, and the two tenants are userA / userB.
98
+ * This is the shape for personal data (notifications, billing records):
99
+ * the invariant is "another *user* must not read these rows", which is the
100
+ * same claim as workspace isolation with a different scope column.
101
+ *
102
+ * Tables classified `user_scoped` in `.proof/schema.json` need `"user"`.
103
+ */
104
+ scope?: "workspace" | "user";
105
+ /**
106
+ * Column carrying the scope value. Defaults to `workspace_id` for workspace
107
+ * scope and `user_id` for user scope.
108
+ *
109
+ * Override when the scope lives on the primary key instead of a foreign key —
110
+ * `workspaces` is scoped by its own `id`, and `users` by `id`.
111
+ */
112
+ scopeColumn?: string;
113
+ /**
114
+ * Planner-owned semantic subset that the fixture must materialize.
115
+ *
116
+ * The equality filter is applied together with tenant scope to service-role
117
+ * ground truth, the owner's positive control, and the outsider probe. Put
118
+ * product meaning here (for example `{ status: "published" }`), not in the
119
+ * executor-owned fixture factory: rows outside this subset do not count as
120
+ * evidence for the claim.
121
+ */
122
+ criterion?: {
123
+ description: string;
124
+ where: Record<string, unknown>;
125
+ };
126
+ /**
127
+ * Optional: pass a `page` to additionally log userB into a browser and
128
+ * assert the UI does not show orgA data. In v0.5 this step only signs in
129
+ * and reloads — callers can add their own page.locator checks afterwards
130
+ * by chaining additional `trace.step` calls.
131
+ */
132
+ page?: Page;
133
+ /**
134
+ * Optional: a tag included in seeded workspace/user names so concurrent
135
+ * runs don't collide on error paths where teardown is skipped.
136
+ */
137
+ tag?: string;
138
+ }
139
+ /**
140
+ * Tenant fixture source.
141
+ *
142
+ * Inline `setup` remains available for small, settled schemas. A proof written
143
+ * before its schema exists should instead import an explicit factory from
144
+ * `e2e/fixtures/<table>.ts`; that file can export `pendingProofFixture(...)`
145
+ * until the builder knows the final constraints. Supplying both is rejected by
146
+ * the type system so there is one source of fixture truth.
147
+ */
148
+ type TenantIsolationOptions = TenantIsolationBaseOptions & ({
149
+ /**
150
+ * Inline setup for an already-known schema. It must create at least one
151
+ * row for side A; side B is optional but required for a bidirectional
152
+ * claim.
153
+ */
154
+ setup: (context: ProofFixtureContext) => Promise<void>;
155
+ fixture?: never;
156
+ } | {
157
+ /** Explicit table-owned factory from `e2e/fixtures/<table>.ts`. */
158
+ fixture: ProofFixtureFactory;
159
+ setup?: never;
160
+ });
161
+ type AuthorizationOp = "select" | "insert" | "update" | "delete";
162
+ interface RlsProbe {
163
+ /** Table the probe operates against. */
164
+ table: string;
165
+ /** Operation the probe attempts. Each op has distinct "denied" semantics. */
166
+ op: AuthorizationOp;
167
+ /**
168
+ * For `select` / `update` / `delete`: `.eq()` filter applied to the query.
169
+ * Required for update/delete (otherwise the probe has no target row) and
170
+ * typical for select (otherwise it's an unqualified scan).
171
+ */
172
+ filter?: Record<string, unknown>;
173
+ /**
174
+ * For `insert` / `update`: the row payload. Ignored for select/delete.
175
+ */
176
+ payload?: Record<string, unknown>;
177
+ /** Trace target override. Defaults to `table`. */
178
+ target?: string;
179
+ }
180
+ interface ActionProbe {
181
+ /** Module part of the registry key: `"<module>:<name>"`. */
182
+ module: string;
183
+ /** Action name part of the registry key. Must be in PROOF_ACTION_REGISTRY. */
184
+ name: string;
185
+ /** Forwarded as-is to the action; validated by its own Zod schema. */
186
+ inputParams: Record<string, unknown>;
187
+ /** Optional regex the rejection message must match for a stricter proof. */
188
+ expectedErrorMatch?: RegExp;
189
+ /**
190
+ * Invariant this action probe establishes. Defaults to `authorization`.
191
+ * Use the action's declared withProof invariant (for example
192
+ * `tenant_isolation` or `idempotency`) so coverage matches the evidence.
193
+ */
194
+ kind?: ProofKind;
195
+ /** Trace target override. Defaults to `"<module>:<name>"`. */
196
+ target?: string;
197
+ }
198
+ interface AuthorizationOptions {
199
+ /**
200
+ * Who is attempting the operation. Must be the role that should be
201
+ * *denied*. Never pass a role the invariant says should succeed —
202
+ * authorization proofs assert rejection, not permission.
203
+ */
204
+ actor: "admin" | "member" | {
205
+ email: string;
206
+ password: string;
207
+ };
208
+ /** RLS-layer probe. At least one of `rls` or `action` is required. */
209
+ rls?: RlsProbe;
210
+ /**
211
+ * Action-layer probe. Requires `page` because the action is invoked via
212
+ * the dev server's POST /api/proof/invoke-action route.
213
+ */
214
+ action?: ActionProbe;
215
+ /** Required when `action` is supplied. */
216
+ page?: Page;
217
+ }
218
+ interface ActionSucceedsOptions {
219
+ /** Who must be able to invoke the action successfully. */
220
+ actor: "admin" | "member" | {
221
+ email: string;
222
+ password: string;
223
+ };
224
+ /** Registered action and the exact input used by the control. */
225
+ action: Omit<ActionProbe, "expectedErrorMatch">;
226
+ /** Required because actions are invoked through the proof Route Handler. */
227
+ page: Page;
228
+ /** Assertion category. Defaults to `happy_path`. */
229
+ kind?: ProofKind;
230
+ /**
231
+ * Positive action checks are controls by default: they prove a related
232
+ * refusal was capable of failing, but cannot satisfy a mission on their own.
233
+ */
234
+ role?: AssertionRole;
235
+ }
236
+ declare const assertMethods: {
237
+ /**
238
+ * Prove that workspace-scoped data in `table` is invisible to members of a
239
+ * different workspace. See file header for full semantics.
240
+ *
241
+ * Records two assertions per proved direction: a `control` showing the org's
242
+ * own member CAN read the rows, and the `primary` showing the outsider
243
+ * cannot. Only the `primary` can satisfy a mission's `trace_must_prove`
244
+ * entry.
245
+ *
246
+ * Fails rather than passing vacuously when `setup()` leaves orgA empty.
247
+ */
248
+ tenantIsolation(opts: TenantIsolationOptions): Promise<void>;
249
+ /**
250
+ * Prove that an operation is rejected when performed by an unauthorized
251
+ * actor. Can probe either the RLS layer (direct Supabase client), the
252
+ * action layer (via POST /api/proof/invoke-action), or both.
253
+ *
254
+ * RLS probes record `kind: "authorization"`. Action probes default to the
255
+ * same kind but may set `action.kind` to the withProof invariant they
256
+ * establish. The helper throws on the first probe that SUCCEEDS when it was
257
+ * expected to fail — that's the PROOF_FAIL signal.
258
+ *
259
+ * Caller is responsible for seeding any fixture rows needed; this helper
260
+ * creates no users or workspaces on its own.
261
+ *
262
+ * @example
263
+ * // Action-layer: member cannot remove admin
264
+ * await assert.authorization({
265
+ * actor: { email: member.email, password: member.password },
266
+ * action: {
267
+ * module: "workspace",
268
+ * name: "removeMember",
269
+ * inputParams: { formData: { workspaceId, memberId } },
270
+ * expectedErrorMatch: /permission|unauthorized/i,
271
+ * },
272
+ * page,
273
+ * });
274
+ *
275
+ * @example
276
+ * // RLS-layer: member cannot DELETE admin's workspace_members row
277
+ * await assert.authorization({
278
+ * actor: { email: member.email, password: member.password },
279
+ * rls: {
280
+ * table: "workspace_members",
281
+ * op: "delete",
282
+ * filter: { user_id: admin.id, workspace_id: orgA.id },
283
+ * },
284
+ * });
285
+ */
286
+ authorization(opts: AuthorizationOptions): Promise<void>;
287
+ /**
288
+ * Prove that a registered action succeeds for an actor who should be allowed.
289
+ *
290
+ * This is the positive-control companion to action-level refusal probes.
291
+ * Assertions default to `role: "control"`, so a working happy path cannot
292
+ * satisfy the mission's primary requirement by itself. Override `role` only
293
+ * when success is the primary invariant being proved.
294
+ *
295
+ * Returns the action's data so the caller can make domain-specific checks.
296
+ */
297
+ actionSucceeds<TData = unknown>(opts: ActionSucceedsOptions): Promise<TData>;
298
+ /**
299
+ * Assert on an HTTP response from a page route. Streaming-RSC aware: when
300
+ * only body markers (`mustContain` / `mustNotContain`) are provided, the
301
+ * helper does NOT check the status code — crucial for routes guarded by
302
+ * a server component's `notFound()` call that Next.js can only signal via
303
+ * an embedded NOT_FOUND marker because headers were already committed as
304
+ * 200 by `loading.tsx`. See
305
+ * [proof-authoring.mdc](mdc:.cursor/rules/proof-authoring.mdc) for the
306
+ * streaming-RSC gotcha in full.
307
+ *
308
+ * Defaults:
309
+ * - method: "GET"
310
+ * - maxRedirects: 0 (intentional — silent redirect-following has caused
311
+ * real false passes on auth-gated pages)
312
+ *
313
+ * Records one TraceAssertion with caller-provided `kind` and `target`.
314
+ * Returns the raw Playwright APIResponse so callers can run additional
315
+ * custom checks (headers, etc.) after.
316
+ *
317
+ * @example
318
+ * // Streaming-RSC page: assert body markers, not status code
319
+ * await assert.httpResponse({
320
+ * page,
321
+ * path: "/secret-admin",
322
+ * as: "member",
323
+ * kind: "authorization",
324
+ * target: "/secret-admin",
325
+ * expect: {
326
+ * mustContain: [/Sorry, we could not find this page/i],
327
+ * mustNotContain: [/Here you can see what.?s happening on your platform/i],
328
+ * },
329
+ * });
330
+ *
331
+ * @example
332
+ * // Flat API route: assert status
333
+ * await assert.httpResponse({
334
+ * page,
335
+ * path: "/api/public-ping",
336
+ * kind: "happy_path",
337
+ * expect: { status: 200 },
338
+ * });
339
+ */
340
+ httpResponse(opts: {
341
+ page: Page;
342
+ path: string;
343
+ kind: ProofKind;
344
+ target?: string;
345
+ /**
346
+ * Evidential role of the recorded assertion. Defaults to `"primary"`.
347
+ * Pass `"control"` when this request exists only to show the probe could
348
+ * have failed — e.g. "the admin CAN reach the page a member was refused" —
349
+ * so it cannot satisfy a mission's `trace_must_prove` entry by itself.
350
+ */
351
+ role?: AssertionRole;
352
+ as?: "admin" | "member" | {
353
+ email: string;
354
+ password: string;
355
+ };
356
+ method?: "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
357
+ expect: {
358
+ status?: number;
359
+ mustContain?: RegExp[];
360
+ mustNotContain?: RegExp[];
361
+ };
362
+ }): Promise<APIResponse>;
363
+ };
364
+ declare const assert: {
365
+ tenantIsolation: (opts: TenantIsolationOptions) => Promise<void>;
366
+ authorization: (opts: AuthorizationOptions) => Promise<void>;
367
+ actionSucceeds: typeof assertMethods.actionSucceeds;
368
+ httpResponse: (opts: {
369
+ page: Page;
370
+ path: string;
371
+ kind: ProofKind;
372
+ target?: string;
373
+ /**
374
+ * Evidential role of the recorded assertion. Defaults to `"primary"`.
375
+ * Pass `"control"` when this request exists only to show the probe could
376
+ * have failed — e.g. "the admin CAN reach the page a member was refused" —
377
+ * so it cannot satisfy a mission's `trace_must_prove` entry by itself.
378
+ */
379
+ role?: AssertionRole;
380
+ as?: "admin" | "member" | {
381
+ email: string;
382
+ password: string;
383
+ };
384
+ method?: "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
385
+ expect: {
386
+ status?: number;
387
+ mustContain?: RegExp[];
388
+ mustNotContain?: RegExp[];
389
+ };
390
+ }) => Promise<APIResponse>;
391
+ };
392
+
393
+ /**
394
+ * Called by `assert.*` helpers while running inside a step. If no step is
395
+ * active (e.g. someone invokes a helper outside `trace.proof`), this is a
396
+ * no-op so the helper still runs without crashing.
397
+ *
398
+ * Spec code may call this directly to record bespoke assertions; those are
399
+ * recorded WITHOUT `emittedBy` (any supplied value is stripped), which marks
400
+ * them as spec-authored claims rather than SDK-verified probes.
401
+ */
402
+ declare function recordAssertion(assertion: TraceAssertion): void;
403
+ declare class TraceRecorderImpl {
404
+ private readonly proofId;
405
+ private readonly steps;
406
+ constructor(proofId: string);
407
+ getSteps(): readonly TraceStep[];
408
+ step<T extends StepResult | void>(opts: StepOptions, fn: () => Promise<T>): Promise<T>;
409
+ }
410
+ declare const trace: {
411
+ /**
412
+ * Start a proof. The callback receives a recorder whose `.step()` method
413
+ * wraps individual observations. When the callback resolves (or throws),
414
+ * a JSON artifact is written to `.proof/traces/<proofId>.json`.
415
+ *
416
+ * The proof rethrows any thrown error so the Playwright test still fails —
417
+ * the trace is written first so you can inspect the failure.
418
+ *
419
+ * @example
420
+ * // Common v0.5 case: no mission attached
421
+ * await trace.proof("auth-login-admin", async (t) => { ... });
422
+ *
423
+ * @example
424
+ * // v1 case: proof belongs to an external mission
425
+ * await trace.proof(
426
+ * { proofId: "auth-login-admin", missionId: "M-042" },
427
+ * async (t) => { ... },
428
+ * );
429
+ */
430
+ proof(arg: string | ProofOptions, fn: (t: TraceRecorderImpl) => Promise<void>): Promise<void>;
431
+ };
432
+
433
+ export { actAsUser, assert, recordAssertion, trace };