diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index cb961e6..1d51ba0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -91,7 +91,7 @@ jobs: strategy: fail-fast: false matrix: - os: [ubuntu-latest, windows-latest] + os: [ubuntu-latest, windows-latest, macos-latest] steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: @@ -134,8 +134,77 @@ jobs: with: name: release-integrity-windows-latest path: dist/windows - - name: Require byte-identical Linux and Windows release artifacts - run: python scripts/release_artifacts.py compare dist/ubuntu dist/windows + - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: release-integrity-macos-latest + path: dist/macos + - name: Require byte-identical Linux, Windows, and macOS release artifacts + run: | + python scripts/release_artifacts.py compare dist/ubuntu dist/windows + python scripts/release_artifacts.py compare dist/ubuntu dist/macos + + platform-comparability-observation: + runs-on: ${{ matrix.runner }} + strategy: + fail-fast: false + matrix: + include: + - runner: ubuntu-latest + platform: Linux + - runner: windows-latest + platform: Windows + - runner: macos-15-intel + platform: Darwin + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 + with: + python-version: "3.12.10" + - run: python -m pip install . + - name: Require the runner's native operating-system view + env: + EXPECTED_PLATFORM: ${{ matrix.platform }} + run: python -c "import os, platform; observed = platform.system(); expected = os.environ['EXPECTED_PLATFORM']; assert observed == expected, (observed, expected)" + - name: Capture, validate, and rerun the portable example + run: | + vstd run examples/generic_run/manifest.json --output dist/platform-comparability/${{ matrix.platform }} + vstd validate dist/platform-comparability/${{ matrix.platform }} + vstd reproduce dist/platform-comparability/${{ matrix.platform }} --rerun + - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: platform-comparability-${{ matrix.platform }} + path: dist/platform-comparability/${{ matrix.platform }} + if-no-files-found: error + retention-days: 14 + + platform-comparability: + needs: [platform-comparability-observation] + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 + with: + python-version: "3.12.10" + - run: python -m pip install . + - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + pattern: platform-comparability-* + path: dist/platforms + merge-multiple: false + - name: Require comparable recorded results across all declared platforms + run: | + vstd compare-platforms \ + dist/platforms/platform-comparability-Linux \ + dist/platforms/platform-comparability-Windows \ + dist/platforms/platform-comparability-Darwin \ + --json > platform-comparison.json + python -c 'import json; from pathlib import Path; report=json.loads(Path("platform-comparison.json").read_text(encoding="utf-8")); assert report.get("status") == "PASS" and report.get("exit_code") == 0, report' + - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: platform-comparison-${{ github.sha }} + path: platform-comparison.json + if-no-files-found: error + retention-days: 14 installed-wheel-smoke: runs-on: ubuntu-latest @@ -196,7 +265,7 @@ jobs: # This identifier remains stable because main branch protection requires it. conformance-gate: if: always() - needs: [base, coverage, stdlib-smoke, scitt-crypto, artifact-seal, release-integrity, release-reproducibility, installed-wheel-smoke, presentation, codeql] + needs: [base, coverage, stdlib-smoke, scitt-crypto, artifact-seal, release-integrity, release-reproducibility, platform-comparability-observation, platform-comparability, installed-wheel-smoke, presentation, codeql] runs-on: ubuntu-latest steps: - name: Require every declared support and artifact check @@ -208,6 +277,8 @@ jobs: ARTIFACT_SEAL: ${{ needs.artifact-seal.result }} RELEASE: ${{ needs.release-integrity.result }} REPRODUCIBLE: ${{ needs.release-reproducibility.result }} + PLATFORM_OBSERVATIONS: ${{ needs.platform-comparability-observation.result }} + PLATFORM_COMPARISON: ${{ needs.platform-comparability.result }} WHEEL: ${{ needs.installed-wheel-smoke.result }} PRESENTATION: ${{ needs.presentation.result }} CODEQL: ${{ needs.codeql.result }} @@ -219,6 +290,8 @@ jobs: test "$ARTIFACT_SEAL" = success test "$RELEASE" = success test "$REPRODUCIBLE" = success + test "$PLATFORM_OBSERVATIONS" = success + test "$PLATFORM_COMPARISON" = success test "$WHEEL" = success test "$PRESENTATION" = success test "$CODEQL" = success diff --git a/CHANGELOG.md b/CHANGELOG.md index 44753b2..eb25605 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,36 @@ > Coordinated Universal Time (UTC); Verifier Standard (VSTD); ZIP archive format (ZIP); > zero-identity/zero-knowledge (ZIZK). +## Unreleased + +### Experimental interoperability planning + +- Add an explicitly experimental `verifier.interoperability` facade for immutable, + domain-neutral component descriptors and registries plus typed VSTD-2 surface-hole + analysis and deterministic, registry-digest-bound candidate plans. Keep these names + outside the supported top-level `verifier.__all__` boundary. +- Add a runnable non-critical sorted-grocery-list example that produces two exact + candidates and leaves three self-closure holes unmatched without invoking its checker. + Planning is not validation execution: strict VSTD-2 wire loading, component execution, + evidence collection, post-execution reanalysis, closure, safety, and critical-domain + readiness remain unsupported or unestablished. + +### Cross-platform comparison + +- Add a supported bounded comparator and `vstd compare-platforms` command for VSTD-1 + generic-run receipts. A declaration alone earns no result: `PASS` requires one + canonically intact receipt per declared operating system, matching non-platform + bindings, and matching declared result projections. Preserve comparable disagreement + as `CONFLICTED`, incomplete or non-comparable evidence as `NOT_ESTABLISHED`, and + malformed or contradictory evidence as `INVALID`. +- Add native GitHub-hosted Linux, Windows, and Intel macOS observation jobs for the + portable generic example and require their aggregate diagnostic in the protected + repository-check gate. Make the example's declared JavaScript Object Notation outputs + use explicit line-feed bytes so Windows text translation cannot create a false portable + surface. Extend release-artifact byte comparison to macOS. These checks + do not establish universal portability, native-execution attestation within the + receipt, semantic correctness, or actor independence. + ## 1.2.0 - 2026-09-01 ### Public surface and integrations diff --git a/README.md b/README.md index c2c6ca8..331e521 100644 --- a/README.md +++ b/README.md @@ -147,7 +147,7 @@ Use this summary before opening the exact matrix: | Usable reference paths | Receipts, generic run capture and reproduction, Graph recording, grounded-certificate checks, evidence-bound assessment, and artifact freeze/seal/thaw mechanisms are implemented within their documented bounds. | | Mechanism-dependent paths | VSTD-4, VSTD-5, and Graph profiles above recorded lineage establish results only when their exact registered mechanisms rerun successfully over bound evidence. | | Compatibility candidates | Candidate depth/profile calculations remain `NOT_ESTABLISHED`; caller-supplied references or ratings cannot create conformance. | -| Experimental integrations | The workflow and Supply Chain Integrity, Transparency, and Trust (SCITT) profiles remain non-normative and grant no VSTD verdict from platform state alone. | +| Experimental integrations | The workflow and Supply Chain Integrity, Transparency, and Trust (SCITT) profiles plus interoperability catalog and surface-planning path remain non-normative. Catalog matching and planning grant no VSTD verdict and execute no checker. | | Not claimed | Accreditation, consensus-standard status, external adoption, a second implementation, a real independent witness, or third-party security review. |
@@ -160,6 +160,7 @@ external interoperability, certification, or a second implementation. |---|---|---|---|---|---| | VSTD-1 | Project specification with implemented reference subset | Claim receipts, checker reports, strict generic-run profile, inspection, and current-profile reads | Claim coordinates, stable digests, mechanism descriptors, and declared provenance; actor separation is not inferred | Implemented reference subset | External implementation and a validator binding distinct producer/checker actors and execution seams | | VSTD-2 | Additive experimental project specification | Typed verification geometry, residuals, closure checks, schema, and tests | Geometry and declared reconstruction evidence inside the receipt | Implemented vertical slice | Independent implementation and broader geometry interoperability | +| Interoperability catalog and surface planning | Experimental implementation; not a receipt or numbered profile | Frozen component descriptors and registry, modeled-hole analysis over typed VSTD-2 geometry, exact candidate matching, and deterministic nonexecuting plans | Binds the typed geometry digest, registry version and digest, match coordinates, prerequisites, blockers, and fixed claim boundary | Plan-only candidate associations; no validation result or closure | Strict full-parity VSTD-2 wire loader, authorized component execution, bound evidence, post-execution reanalysis, and independent interoperability results | | VSTD-3 | Implemented project specification | Typed accelerator model, strict validator, emulator, offline adapters, continuity, fleet, and claim evaluation | Conditional on source-specific signatures, nonces, reference values, topology, events, and trust roots; host inventory remains weak evidence | Implemented reference surface | Vendor firmware integration, production trust roots, and complete-mediation evidence outside the emulator boundary | | VSTD-4 | Project specification with implemented reference paths | grounded decision certificate (GDC) parser/kernel, compatibility candidate depth, and evidence-bound establishment/recheck | Exact VSTD-1/2/3 and fourteen-rung propositions, content-addressed evidence bytes, mechanism implementation digests, trust roots, and bounds | Candidate path `NOT_ESTABLISHED`; evidence-bound path can establish conformance | Independent implementation, external interoperability, and deployment-specific rung mechanisms/evidence | | VSTD-5 | Project specification with implemented reference mechanism | Evidence-bound entry gate, seven separation dimensions, exact admitted-certificate binding, corroboration checks, duplicate refusal, disagreement preservation, receipt build/recheck | Witness coordinate, exact negative separation propositions, VSTD-4 commitment/certificate, checker, observations, mechanisms, trust roots, bounds, and embedded evidence | Mechanism can establish a bounded result; a positive observation with unresolved independence remains overall `UNKNOWN`; no repository claim of a real independent witness | Real independent witnesses, second implementation, external attack, and operational interoperability | @@ -168,7 +169,7 @@ external interoperability, certification, or a second implementation. | VSTD-Graph-3 | Project specification with implemented reference paths | Compatibility candidate plus evidence-bound Accountable Provenance Closure computation/recheck | Same complete closure binding, including VSTD-3 rating propositions | Candidate `NOT_ESTABLISHED`; evidence-bound path can establish | Production VSTD-3 rating evidence across a real collection | | VSTD-Graph-4 | Project specification with implemented reference paths | Compatibility candidate plus evidence-bound Refutable Transformation Closure computation/recheck | Same complete closure binding; an edge mechanism must actually check its refutability closure | Candidate `NOT_ESTABLISHED`; evidence-bound path can establish | External closure mechanisms and independent replay | | VSTD-Graph-5 | Project specification with implemented reference paths | Compatibility candidate plus evidence-bound Corroborated Verification Network computation/recheck | Exact VSTD-5 object and transformation rating mechanisms across the complete closure | Candidate `NOT_ESTABLISHED`; evidence-bound path can establish | Real independently corroborated collection, second implementation, and interoperability | -| Generic run | VSTD-1 generic-computation profile | Plan, execute, capture, inspect, strict shape/digest validation, and declared-output rerun | Captures command, source state, outputs, environment, and manifest declarations; generic validation is not native claim verification or VSTD-4 conformance | Implemented VSTD-1 profile | Sandbox, generic external-evidence resolver, and actor/execution binder | +| Generic run | VSTD-1 generic-computation profile | Plan, execute, capture, inspect, strict shape/digest validation, declared-output rerun, and bounded cross-platform result comparison | Captures command, source state, outputs, environment, and manifest declarations; platform comparison requires complete declared coverage and matching non-platform bindings; generic validation is not native claim verification or VSTD-4 conformance | Implemented VSTD-1 profile plus an additive diagnostic comparator | Sandbox, generic external-evidence resolver, native-execution attestation, and actor/execution binder | | Artifact freeze, seal, and thaw | Normative artifact-control mechanism; not a numbered VSTD or receipt profile | Exact regular-file byte preservation, dual-digest artifact identity, read-only guards, finite self-closing Ed25519 seals, external anchor checks, and copy-on-write thaw status | Binds artifact bytes, paths, media type, freeze manifest, carried key, signature, and optional expected artifact/key coordinates | Implemented mechanism version 1 | Durable external archive, privileged-write prevention, trusted time, encryption, semantic correctness, and realm/continuity verification | | Experimental workflow | Non-normative experimental profile 0.1 | Strict validator, verdict-neutral GitHub event projector, allocation records, and command-line interface (CLI) | Preserves native platform results and explicit horizons with `verification_effect = NONE` | No VSTD conformance claim | Independent consumer, additional platform adapter, and evidence for allocation optimality | | Supply Chain Integrity, Transparency, and Trust (SCITT) interoperability | Experimental, non-normative application profile and crosswalk | Real local Concise Binary Object Representation (CBOR) plus CBOR Object Signing and Encryption (COSE) signatures/receipt, loss-declared adapter, and adjacent native-result composition | Binds the exact payload under emitted test keys and local policy; registration never establishes payload truth | VSTD-4 remains `NOT_ESTABLISHED` | Public Transparency Service, external implementation/interoperability result, and Internet Engineering Task Force (IETF) review | @@ -336,8 +337,24 @@ Generic `validate` checks the strict profile shape and stable-payload digest. It not rehash external artifacts, resolve evidence references, rerun the command, or verify the recorded declaration as a native domain claim. `reproduce --rerun` separately executes the recorded command and compares declared output paths, digests, and execution -outcome. Matching outputs do not establish actor independence, environment equivalence, -semantic equivalence, or truth outside that scope. +outcome. + +When a manifest's open refutation surface declares compatible platforms and explicit +result surfaces, compare one receipt from each declared operating system: + +```bash +vstd compare-platforms receipts/Linux receipts/Windows receipts/Darwin --json +``` + +Python reports macOS as `Darwin`. The comparator returns `PASS` only when every declared +platform appears exactly once, canonical receipt integrity passes, non-platform claim, +source, command, Python, machine-family, and mechanism bindings agree, and every declared +result projection agrees. Comparable result disagreement is `CONFLICTED`; missing or +non-comparable evidence is `NOT_ESTABLISHED`; malformed or internally contradictory +evidence is `INVALID`. This is a comparison of supplied recorded results—not proof of +universal portability, semantic correctness, native execution, or independent actors. +Matching outputs alone do not establish environment equivalence or truth outside that +scope. ### Use the Python application programming interface (API) @@ -385,6 +402,33 @@ native object ──native verifier──> native result └──> VSTD claim boundary ``` +The experimental interoperability-planning path connects a domain-neutral component +catalog to detection and planning: + +```text +typed VSTD-2 geometry + -> modeled-hole analysis + -> exact catalog candidates + -> nonexecuting validation plan +``` + +Run its harmless sorted-grocery-list example: + +```bash +python examples/interoperability_planning/demo.py +``` + +The example reports two exact candidates and three unmatched self-closure holes while +keeping `plan_only = true`, `execution_performed = false`, and the checker invocation +count at zero. Here, **validation** names the future process of attempting to discharge +verification-surface holes with bound execution evidence and then reassessing the +geometry. This example only detects and plans: it cannot establish checker availability +at execution time, a native result, ordinary or self-closure, safety, authority to act, +critical-domain readiness, or VSTD conformance. Its registry version and digest are bound +into the plan. Strict VSTD-2 wire loading, automatic execution, and a command-line +interface remain unsupported. See the +[example boundary and output](examples/interoperability_planning/). + The experimental SCITT profile uses real Concise Binary Object Representation (CBOR) and COSE signatures and a local inclusion receipt. It demonstrates exact payload carriage and @@ -423,8 +467,11 @@ Additional entry points: A release contains a canonical artifact set: ZIP archive format (ZIP), wheel, source distribution, and external manifest bound to the exact public Git commit and file -members. The continuous integration (CI) workflow builds on Windows and Linux and rejects -cross-platform byte differences. GitHub +members. At the current unreleased source coordinate, the continuous integration (CI) +workflow builds the artifact set on Linux, Windows, and macOS and rejects cross-platform +byte differences. It also captures and reruns the portable generic example on three +GitHub-hosted operating-system virtual machines, then requires a bounded `PASS` over the +three receipts. GitHub artifact attestations bind uploaded bytes to the workflow; they do not establish source correctness, tag identity, or adoption. diff --git a/RELEASING.md b/RELEASING.md index 75012bf..d603bde 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -55,7 +55,7 @@ release-candidate Zenodo metadata. itself, avoiding self-reference. The protected repository-check aggregate separately builds this complete artifact set - on Windows and Linux and compares every byte. Do not prepare a tag unless that + on Linux, Windows, and macOS and compares every byte. Do not prepare a tag unless that cross-platform comparison passed on the exact candidate commit. 4. Run `twine check` on the candidate wheel and source distribution. Install the diff --git a/ROADMAP.md b/ROADMAP.md index ea18d63..30d1af8 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -201,6 +201,27 @@ shared interoperability vocabulary without redefining VSTD outcomes. Whether an belongs in an optional module or a separately governed repository remains a future coupling and ownership decision. +### Experimental catalog-to-plan integration + +The current unreleased implementation connects a domain-neutral component catalog to +surface detection and planning without adding a receipt field or executing a component: + +```text +typed VSTD-2 geometry + -> modeled-hole analysis + -> exact catalog candidates + -> nonexecuting validation plan +``` + +The plan binds the geometry digest plus registry version and digest. Domain tags support +grouping and discovery but cannot create a capability match; schema, interaction mode, +relation, and mechanism coordinates must match exactly where applicable. **Validation** +remains the future process of attempting to discharge holes with bound execution evidence +and then reassessing the geometry. Strict full-parity VSTD-2 wire loading, component +execution, evidence acquisition, contradiction adjudication, and post-execution +reanalysis are later integration gates. No candidate association establishes closure, +safety, authority to act, or critical-domain readiness. + ## Current experimental development tracks This dated register records substantive work as of **2026-08-29**. A committed experiment, @@ -211,6 +232,7 @@ intentional experiment artifacts are present. | Track | Public artifact | Current boundary | Next gate | |---|---|---|---| +| Interoperability catalog and surface planning | [`examples/interoperability_planning/`](examples/interoperability_planning/) | Experimental immutable descriptors and registry, typed modeled-hole analysis, exact candidate matching, and registry-bound nonexecuting plans; no checker execution or closure result. | Strict VSTD-2 wire loader, execution authorization and evidence contract, post-execution reanalysis, and independent interoperability specimens. | | SCITT interoperability | [`docs/standards/VSTD_SCITT_CROSSWALK.md`](docs/standards/VSTD_SCITT_CROSSWALK.md) | Experimental adapter, rerunnable real-COSE specimen with ephemeral keys, and adversarial tests; no IETF review or external interoperability result. | Independent implementation and interoperability result. | | Artifact-first mechanism completion | [`standard/schemas/vstd-graph-assurance-1.schema.json`](standard/schemas/vstd-graph-assurance-1.schema.json) | Event serialization, evidence-bound TRUST/ROT/RUST dispatch, challenge projection, conflict resolution, structural concentration, explicit localization, and bounded diagnostic attribution are implemented and adversarially tested. The complete domain-independent transfer algebra, complete trichotomy derivation, cross-implementation replay, and specific optional proof backends remain open. | Supply and falsify real domain mechanisms without creating actor-tied trust or topology-derived assurance. | | Workflow and allocation | [`docs/profiles/experimental-workflow.md`](docs/profiles/experimental-workflow.md) | Strict validator, verdict-neutral GitHub adapter, generated index, and allocation records; no optimality claim or independent consumer. | A second observable adapter and independent consumer. | diff --git a/docs/API_STABILITY.md b/docs/API_STABILITY.md index 58cdc34..4f89266 100644 --- a/docs/API_STABILITY.md +++ b/docs/API_STABILITY.md @@ -55,6 +55,14 @@ The matching supported portable-record exports are Compatibility `vstd4_depth` and `graph_level`-style candidate results do not become conformance results merely because the evidence-bound APIs also exist. +At the current unreleased source coordinate, `compare_platform_run_receipts`, +`PlatformComparisonResult`, and `PlatformComparisonStatus` are supported additions for +the next minor release's bounded operating-system comparison API. +They operate on existing VSTD-1 generic-run receipts and return a diagnostic object, not +a new receipt or conformance result. `PASS` requires complete declared platform coverage, +canonical integrity, equal non-platform bindings, and equal declared result projections; +`CONFLICTED`, `NOT_ESTABLISHED`, and `INVALID` remain distinct failure states. + `assess_witness_corroboration` accepts incomplete inputs so it can return a typed diagnostic result. The supported `build_vstd5_receipt` boundary is stricter: it either raises or returns an object satisfying the published receipt shape with all verdict-material evidence bytes. @@ -71,6 +79,40 @@ historical overlap remains readable but cannot enter evidence-bound Graph establ assurance mechanisms. The compatibility candidate computation retains its historical scope and remains `NOT_ESTABLISHED`. +### Experimental interoperability facade + +`verifier.interoperability` is an explicitly experimental import facade outside the +supported `verifier.__all__` compatibility boundary. Its characterized names at the +current unreleased source coordinate are: + +```text +CandidateStatus +CatalogError +ComponentAvailability +ComponentKind +ComponentLifecycle +ControlSurfaceContext +InteractionMode +InteroperabilityComponentDescriptor +InteroperabilityComponentRegistry +SurfaceAnalysis +SurfaceAnalysisError +SurfaceHole +SurfaceHoleKind +ValidationCandidate +ValidationPlan +analyze_verification_surface +plan_validation +``` + +The redundant internal aliases `InteroperabilityCatalog` and +`generate_validation_plan` are intentionally not facade exports. These experimental +names may change in a minor release and must not be inferred to be top-level supported +API. They accept typed VSTD-2 geometry, detect holes only inside the supplied modeled +surface, match exact declared component capabilities, and produce registry-bound, +nonexecuting plans. The facade provides no strict VSTD-2 wire loader, command-line +interface, component executor, evidence collector, or closure result. + Direct imports from `verifier.core`, `verifier.data`, `verifier.hardware`, other subpackages, or underscore-prefixed names are internal unless another published policy explicitly names them. They may change in a minor release. That freedom does not override diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 45ea4ba..8b67f00 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -28,7 +28,7 @@ shape only; a passing validator establishes only its named implemented checks. | Coordinate | Normative source | Runtime owner | Published shape | Primary tests | |---|---|---|---|---| | VSTD-1 claim receipt | `standard/VSTD-1.md` | `verifier.core.receipt`, `verifier.core.checker` | `vstd1_receipt.json` | `test_independent_checker.py`, `test_vstd_schemas.py` | -| VSTD-1 generic run | `standard/VSTD-1.md` | `verifier.core.run` capture/facade plus `run_planning`, `run_validation`, `run_inspection`, `run_reproduction`, and `run_impact` | `vstd1_generic_run_receipt.json` | `test_generic_run.py` | +| VSTD-1 generic run | `standard/VSTD-1.md` | `verifier.core.run` capture/facade plus `run_planning`, `run_validation`, `run_inspection`, `run_reproduction`, `run_impact`, and the additive `platform_comparison` diagnostic | `vstd1_generic_run_receipt.json`; the comparison report is not a receipt | `test_generic_run.py`, `test_platform_comparison.py` | | VSTD-2 | `standard/VSTD-2.md` | `verifier.core.geometry` | `vstd2_receipt.json` | `test_verification_geometry.py` | | VSTD-3 | `standard/VSTD-3.md` | `verifier.hardware` | `vstd3_receipt.json`, `vstd3_accelerator_profile.json` | `test_vstd3_schema.py`, hardware tests | | VSTD-4 | `standard/VSTD-4.md` | certificate/kernel checks plus candidate and evidence-bound paths in `verifier.core.depth` / `verifier.core.evidence` | `vstd4_certificate.json`, `vstd4_receipt.json` | `test_gdc_certificate.py`, `test_vstd4_depth.py`, `test_evidence_bound_assurance.py` | @@ -110,6 +110,39 @@ native computation -> later bounded verification loop ``` +### Experimental interoperability planning side path + +The current unreleased source also exposes a deliberately nonexecuting planning side +path: + +```text +typed VSTD-2 geometry + -> modeled-hole analysis + -> exact catalog candidates + -> nonexecuting validation plan +``` + +`analyze_verification_surface` accepts only a typed `VerificationGeometry`, validates +its structure, assesses its declared ordinary and self-closure state, and converts +existing blockers into typed holes. It does not infer an omitted expected profile, +ontology, coordinate, or real-world surface. Strict full-parity VSTD-2 wire loading is +unsupported. + +`plan_validation` connects those holes to immutable component descriptors through exact +schema and interaction-mode coordinates plus the hole's relation and mechanism +coordinates where present. Domain tags, consequence profiles, and operating-regime text +remain organization and planning metadata; they cannot manufacture a match. A plan binds +the geometry digest plus registry version and digest, retains execution prerequisites and +blockers, and fixes `plan_only` to true and `execution_performed` to false. + +In this architecture, **validation** is the future process of attempting to discharge +holes with authorized component execution and bound evidence, followed by a fresh surface +analysis. Candidate matching alone does not establish component availability at execution +time, checker correctness, a native result, ordinary or self-closure, safety, authority, +critical-domain readiness, or VSTD conformance. The runnable +[sorted-grocery-list example](../examples/interoperability_planning/) intentionally leaves +three self-closure requirements unmatched and never calls its checker. + VSTD-2 is the semantic target for adjacent adapters, not the adapter implementation itself. Geometry profiles constrain reusable selections of VSTD-2 geometry; they are connected only by explicit shared coordinates, seams, mappings, and evidence-bearing transformations. @@ -232,6 +265,20 @@ identifier as a generic container name. | prior commitment | Assurance input | Records a commitment string; receipt inclusion does not prove temporal priority. | | refutation surface | Attribution | Declares admissible refutations and exclusions; it is not the checked VSTD-4 `RefutationSurface`. | +The open refutation surface may carry a `platform_comparability` declaration naming the +compared subject mechanism, compatible operating systems, and result surfaces. The +declaration remains data until `compare_platform_run_receipts` or +`vstd compare-platforms` validates one +canonically intact receipt per declared system, verifies equal non-platform bindings, +and compares the declared result projections. Missing or duplicate platforms and binding +drift remain `NOT_ESTABLISHED`; malformed or contradictory receipts are `INVALID`; only +result disagreement after comparability is established is `CONFLICTED`. `PASS` establishes +agreement only for the supplied recorded results. Python serializes macOS as `Darwin`. +The comparator also binds normalized machine family so the current hosted Linux, Windows, +and Intel macOS observation isolates the operating-system dimension more narrowly. It +does not attest native execution, virtual-machine identity, semantic correctness, +universal portability, or actor independence, and it does not create a new receipt schema. + Closure coordinates identify assessment questions; they are not containers for generic verification context. The neutral container must not generate profile-numbered binding structures. Nothing in diff --git a/docs/CLAIMS_AND_LIMITS.md b/docs/CLAIMS_AND_LIMITS.md index 084de29..704774d 100644 --- a/docs/CLAIMS_AND_LIMITS.md +++ b/docs/CLAIMS_AND_LIMITS.md @@ -41,9 +41,15 @@ checker result—not a claim of universal truth or whole-project conformance. | What can VSTD-Graph establish? | Stored topology plus a candidate over supplied ratings, or an evidence-bound Graph profile after every complete-closure rating mechanism is rerun. | Graph bytes, lifecycle/conflict view, exact rating bindings, embedded evidence, mechanisms, roots, bounds, and certificate. | Recorded topology is not complete real-world causality; the compatibility path remains `NOT_ESTABLISHED`, and domain mechanism correctness remains a declared trust boundary. | | What can VSTD-3 establish? | Conditional device, firmware, execution, accounting, continuity, or fleet predicates when each required evidence path validates. | Named roots, keys, nonces, measurements, topology, events, appraisal inputs, and profile-specific validators. | Host inventory is not attestation; production vendor integration and complete mediation outside the emulator remain separate requirements. | | What can artifact control establish? | Current exact file bytes and paths match a freeze manifest; an optional finite seal closes that freeze; with an actual supplied and cleanly verified parent whose recorded coordinates agree, a thawed descendant currently matches or differs from that parent. | Preserved bytes, SHA-256 and SHA3-256 commitments, read-only payload-tree guard, Ed25519 signature, artifact-derived identifiers, supplied parent bundle, any supplied external artifact/key anchor, fail-closed final-entry classification for supported creation paths, and ordinary lexical-type checks for authoritative internal bundle members. | Read-only is not privileged access control; a seal is not encryption, correctness, trusted time, ownership, durable external archiving, or a numbered-profile result. A thaw sidecar alone does not authenticate a parent or historical copy operation. Outer read aliases remain distinct from internal closure, ordinary hard links do not prove exclusive inode ownership, and path checks do not establish universal race-free, mount-independent, or network-filesystem security. | +| What can experimental interoperability planning establish? | Which holes already represented by one structurally valid typed VSTD-2 geometry have exact declared candidates in one bound component registry. | Geometry and registry digests, exact schema, interaction-mode, relation, and mechanism coordinates, component descriptors, prerequisites, blockers, and the nonexecuting planner. | A candidate is not checker execution, evidence, a native result, validation, closure, safety, authority to act, critical-domain readiness, or VSTD conformance. Strict VSTD-2 wire loading remains unsupported. | | What does SCITT add? | Signature and registration/inclusion evidence for exact payload bytes under a declared relying-party policy. | Native SCITT verifier, issuer/log keys, payload digest, registration policy, and Transparency Service evidence. | Registration cannot establish payload correctness, VSTD conformance, or issuer authority outside the policy. The current example uses a local test log. | | What remains outside current support? | General AI safety, hidden state, complete physical-world history, automatic real-world actor independence, unrecorded provenance, universal support algebra, and unqualified truth. | VSTD-5 and Graph assurance now dispatch exact evidence-bound mechanisms; they do not manufacture the missing domain observations or external witnesses. | Preserve `UNKNOWN`, `UNSUPPORTED`, `CONFLICTED`, or `NOT_ESTABLISHED`; do not infer a clean result. | +In the experimental interoperability path, **validation** names the future process of +attempting to discharge verification-surface holes with authorized component execution +and bound evidence, followed by reanalysis. Detection and planning alone are not that +process. + Every claim below expands one of these boundaries into publishable wording and its required falsification surface. @@ -64,9 +70,11 @@ required falsification surface. | “All recorded target ancestors are explicitly `VALID`.” | **Yes, if the fail-closed valid-ancestor policy passes.** | That policy rejects every recorded target ancestor not explicitly marked `VALID`. | Target artifact, ancestor closure, status evidence, passing `POL-ALL-ANCESTORS-VALID`. | The status declarations are authentic or that unrecorded ancestors do not exist. | | “The recorded SPDX metadata matches the allowlist.” | **Yes, if the exact metadata policy passes.** | The policy compares recorded license identifiers with the declared allowlist. | Rights records, roots, allowlist, passing policy result. | Copyright ownership, license authenticity, compatibility, fair use, or a legal ruling. | | “This result reproduced bitwise.” | **Yes, for the declared outputs after a passing rerun.** | The rerun produced byte-identical declared output artifacts. | Original receipt, runnable command, captured inputs, environment boundary, rerun outputs, byte comparison. | All environments will reproduce it or the computation is empirically correct. | +| “These Linux, Windows, and macOS results are cross-platform comparable.” | **Yes, only for the supplied declared surfaces after the comparator returns `PASS`.** | Every declared platform supplied one canonically intact generic-run receipt; non-platform claim, source, command, Python, normalized machine-family, and mechanism bindings matched; and the declared recorded result projections matched. Python records macOS as `Darwin`. | All declared receipts, their canonical digests, the shared `platform_comparability` declaration, comparison-binding digest, result digests, exact comparator implementation, and hosted-runner evidence when native operating-system execution is claimed. | The declaration proved compatibility, all machines or versions behave identically, the result is semantically correct, the receipt itself attests native execution, or independent actors performed the runs. Missing or non-comparable evidence remains `NOT_ESTABLISHED`; comparable disagreement is `CONFLICTED`. | | “This was independently verified.” | **Only when distinct producer and checker actors plus the relevant execution seams are evidenced.** | Matching results establish artifact agreement, not who performed either run. Actor independence, implementation separation, runtime separation, and the trusted computing base must be recorded separately. | Evidence binding distinct actors to the producer and checker runs, implementation/runtime isolation, trusted computing base, and the checker result. | Two runs, two processes, two machines, or matching outputs automatically prove independent actors. | | “This verification surface is self-closed.” | **Only if every VSTD-2 self-closure condition passes.** | Self-closure requires ordinary closure, resolved material residuals, discharged valences, post-verified mechanisms, no unresolved trust-root horizon, and contiguous verification orders. | Complete geometry document and passing closure assessment with no blockers. | Universal truth, infinite regress closure, permanent validity, or verification outside the surface. | | “This competition submission and score are bound together.” | **Yes, conditionally.** | A receipt can bind identified submission bytes, evaluator version, raw metrics, and deterministic score derivation. | Submission digest, evaluator/scorer identity, environment, raw metrics, score rule, receipt. | Hidden-test integrity, no leakage, leaderboard ranking, prize eligibility, or organizer acceptance. | +| “This registered component is an exact candidate for this modeled surface hole.” | **Yes, only after exact catalog matching in a registry-bound plan.** | The component's declared schema and interaction mode plus the hole's relation and mechanism coordinates, where present, matched exactly; domain tags did not participate. | Structurally valid typed geometry, geometry digest, exact hole, complete component descriptor, registry version and digest, planner implementation, candidate status, prerequisites, and blockers. | The component ran, was available at execution time, produced evidence, discharged the hole, was independently checked, established closure or safety, or is ready for a critical domain. | | “This native verifier result was mapped into VSTD.” | **Yes, when the mapping preserves the native object, result, trust roots, bounds, and unsupported fields.** | VSTD can standardize the claim boundary and portable result semantics around a domain verifier without performing that verifier's native work. | Native object and version, native verifier implementation/version, native result, per-field mapping, information-loss declaration, VSTD coordinate, adapter tests. | VSTD replaced or reimplemented the native verifier, strengthened its result, inherited its authority, or established conformance to the source standard. | | “A challenge to this recorded ancestor affects these recorded descendants.” | **Yes, as a bounded reassessment surface.** | `project_challenges` reruns the built-in projection over complete challenge records; `impacted_descendants` deduplicates forward reachability; current TRUST records depending on the now-inadmissible ancestor are excluded. | Challenged artifact ID, bound hypergraph, complete challenge records, and replayed assurance log. | Historical receipts or TRUST events were mutated, every descendant is false, or unrecorded downstream systems were found. | | “This artifact has bounded technical GUILT for this deviation.” | **Only after component composition passes.** | The reference ledger requires separately bound passing responsibility, exact obligation-applicability, and same-obligation violation evaluations whose artifact, deviation, localization, and scope coordinates agree; the final mechanism binds their exact digests. | Exact artifact and descendant IDs, passing localization and RUST lineage, typed obligation coordinate and scope, all three component events and evidence, mechanisms and implementation digests, trust roots, bounds, final composition, and successful replay. | Moral character, actor reputation, social scoring, automatic legal liability, innocence or exoneration when absent, obligation satisfaction, or absence of hidden contributors. | diff --git a/docs/reference.html b/docs/reference.html index a64118f..3b78c23 100644 --- a/docs/reference.html +++ b/docs/reference.html @@ -117,6 +117,14 @@

vstd plan

--jsonoptional +
+

vstd compare-platforms

+

Compare declared generic-run result surfaces across operating systems.

+ + + +
ArgumentKindMeaning
receiptspositionalReceipt directories or receipt.json files, one per declared platform.
--jsonoptional
+

vstd validate

Run implemented receipt checks; Graph candidate validation is not conformance.

@@ -535,6 +543,21 @@

ObligationCoordinate class

to_dict(self) -> 'dict[str, Any]'
+
+

PlatformComparisonResult class

+
PlatformComparisonResult(status: 'PlatformComparisonStatus', reason: 'str', declaration: 'dict[str, Any] | None', required_platforms: 'tuple[str, ...]', observed_platforms: 'tuple[str, ...]', comparison_binding_digest: 'str | None', observations: 'tuple[dict[str, Any], ...]', differences: 'tuple[dict[str, Any], ...]', errors: 'tuple[str, ...]') -> None
+

Machine-readable diagnostic result; this object is not a VSTD receipt.

+

Defined in verifier.core.platform_comparison

+ + +
MethodSummary
to_dict(self) -> 'dict[str, Any]'
+
+
+

PlatformComparisonStatus enum

+

Typed outcome of a bounded platform comparison.

+

Defined in verifier.core.platform_comparison

+

Members: PASS, CONFLICTED, NOT_ESTABLISHED, INVALID

+

ProvenanceHypergraph class

ProvenanceHypergraph() -> 'None'
@@ -664,6 +687,13 @@

claim_binding_from_dict functionReconstruct the exact VSTD-4 claim binding carried by a receipt.

Defined in verifier.core.depth

+

+
+

compare_platform_run_receipts function

+
compare_platform_run_receipts(receipts: 'Iterable[str | Path]') -> 'PlatformComparisonResult'
+

Compare declared result surfaces across canonically intact run receipts.

+

Defined in verifier.core.platform_comparison

+

compute_canonical_digest function

diff --git a/examples/generic_run/compute.py b/examples/generic_run/compute.py index accd052..a6700c0 100644 --- a/examples/generic_run/compute.py +++ b/examples/generic_run/compute.py @@ -31,11 +31,13 @@ def main() -> int: # deterministic regardless of Python's dict/Counter iteration order. frequency_table = sorted(counts.items(), key=lambda kv: (-kv[1], kv[0])) - with open(output_path, "w", encoding="utf-8") as f: + # Explicit line-feed serialization prevents the host text layer from + # translating output bytes on Windows. + with open(output_path, "w", encoding="utf-8", newline="\n") as f: json.dump({"frequency_table": frequency_table}, f, indent=2, sort_keys=True) f.write("\n") - with open(metrics_path, "w", encoding="utf-8") as f: + with open(metrics_path, "w", encoding="utf-8", newline="\n") as f: json.dump({"total_tokens": len(tokens), "unique_tokens": len(counts)}, f, indent=2, sort_keys=True) f.write("\n") diff --git a/examples/generic_run/manifest.json b/examples/generic_run/manifest.json index 484f47a..d8a5646 100644 --- a/examples/generic_run/manifest.json +++ b/examples/generic_run/manifest.json @@ -7,7 +7,8 @@ "limitations": [ "This is a deliberately small worked example chosen for zero external dependencies and exact output comparison, not a claim about any production model, dataset, or benchmark.", "The computation avoids floating point and hash-order-dependent output, but this generic capture path does not independently verify determinism or bind a complete execution environment.", - "No external evaluation evidence is claimed anywhere in this receipt — it is a purely local, self-contained computation." + "No external evaluation evidence is claimed anywhere in this receipt — it is a purely local, self-contained computation.", + "A passing cross-platform comparison covers only the supplied receipts and declared result surfaces; it does not prove universal portability or native execution attestation." ], "falsification_condition": "A `vstd validate` digest mismatch falsifies stable-content integrity. A `vstd reproduce --rerun` output mismatch falsifies byte-identical reproducibility for that rerun, not the recorded original execution by itself." }, @@ -39,5 +40,23 @@ } ], "external_evaluation": null, - "provenance_roots": [] + "provenance_roots": [], + "refutation_surface": { + "admissible_refutations": [ + "A canonically intact receipt from a declared platform records a different declared result projection under the same non-platform binding.", + "A declared platform is missing or represented more than once.", + "The non-platform receipt bindings differ, so operating-system isolation is not established." + ], + "excluded_claims": [ + "UNIVERSAL_CROSS_PLATFORM_COMPATIBILITY", + "NATIVE_EXECUTION_ATTESTATION", + "PHYSICAL_WORLD_COMPLETENESS" + ], + "falsification_condition": "After every declared platform supplies one canonically intact receipt with an identical non-platform binding, any difference in the declared result projection yields CONFLICTED.", + "platform_comparability": { + "mechanism_id": "VSTD-GENERIC-WORD-FREQUENCY-1", + "compatible_platforms": ["Linux", "Windows", "Darwin"], + "result_surfaces": ["execution", "declared_outputs", "evaluator_claims", "stdio"] + } + } } diff --git a/examples/interoperability_planning/README.md b/examples/interoperability_planning/README.md new file mode 100644 index 0000000..2752905 --- /dev/null +++ b/examples/interoperability_planning/README.md @@ -0,0 +1,34 @@ +# Experimental interoperability planning example + +This runnable, non-critical example models one question: whether a grocery list is in +Python's default lexicographic string order. It then detects holes in a typed Verifier +Standard (VSTD)-2 geometry and matches those holes against one experimental checker +descriptor: + +```text +typed VSTD-2 geometry + -> modeled-hole analysis + -> exact catalog candidates + -> nonexecuting validation plan +``` + +After installing this checkout, for example with `python -m pip install -e .`, run: + +```bash +python examples/interoperability_planning/demo.py +``` + +The deterministic report contains five modeled holes, two exact component candidates, +and three unmatched self-closure requirements. It also binds the registry version and +digest into the plan. `plan_only` is `true`, `execution_performed` is `false`, and the +checker invocation count remains zero. + +Here, **validation** names a future process that would attempt to discharge verification +surface holes using bound execution evidence and then reassess the geometry. This example +only detects and plans. Catalog membership does not establish availability at execution +time, checker correctness, a native result, ordinary or self-closure, safety, authority to +act, critical-domain readiness, or Verifier Standard conformance. + +The example accepts a typed in-memory `VerificationGeometry`. Strict VSTD-2 wire loading, +a command-line interface, component execution, and post-execution reanalysis remain +unsupported. diff --git a/examples/interoperability_planning/demo.py b/examples/interoperability_planning/demo.py new file mode 100644 index 0000000..f3f17bd --- /dev/null +++ b/examples/interoperability_planning/demo.py @@ -0,0 +1,210 @@ +"""Terminology: identifier (ID); Verifier Standard (VSTD). + +Harmless, plan-only interoperability example over a sorted grocery list. + +The native checker below is a real callable, but this example deliberately does +not invoke it. It detects modeled VSTD-2 surface holes and associates exact +experimental catalog candidates; it does not perform validation or establish +closure. +""" + +from __future__ import annotations + +import json +from typing import Any + +from verifier.core.geometry import ( + Coordinate, + CoordinateJudgment, + CoordinateStatus, + Facet, + Grain, + Locus, + LocusKind, + Stratum, + Subject, + VerificationGeometry, + VerificationMechanism, + VerificationSurface, +) +from verifier.interoperability import ( + CandidateStatus, + ComponentAvailability, + ComponentKind, + ComponentLifecycle, + InteractionMode, + InteroperabilityComponentDescriptor, + InteroperabilityComponentRegistry, + analyze_verification_surface, + plan_validation, +) + + +CHECKER_MECHANISM_ID = "mechanism:lexicographic-check" +_checker_invocations = 0 + + +def check_lexicographic_order(items: object) -> str: + """Return the checker's native result for a sequence of grocery-item strings.""" + + global _checker_invocations + _checker_invocations += 1 + if not isinstance(items, (list, tuple)) or not all( + isinstance(item, str) for item in items + ): + return "INVALID_INPUT" + return "ORDERED" if list(items) == sorted(items) else "OUT_OF_ORDER" + + +def build_geometry() -> VerificationGeometry: + """Build the minimum typed geometry whose open coordinate is catalog-matchable.""" + + subject_id = "subject:sorted-grocery-list" + locus_id = "locus:grocery-list-sort" + facet_id = "facet:lexicographic-order" + coordinate_id = "coordinate:grocery-list-lexicographic-order" + return VerificationGeometry( + geometry_id="geometry:sorted-grocery-list", + primary_subject_id=subject_id, + subjects=[ + Subject( + subject_id=subject_id, + label="Sorted grocery list", + version="1", + ) + ], + loci=[ + Locus( + locus_id=locus_id, + subject_id=subject_id, + label="Grocery-list sorting function", + kind=LocusKind.FUNCTION, + grain=Grain.FUNCTION, + stratum=Stratum.OUTPUT, + address="examples/interoperability_planning/demo.py:check_lexicographic_order", + ) + ], + facets=[ + Facet( + facet_id=facet_id, + label="Declared lexicographic order", + description=( + "Whether the supplied grocery-item strings occur in Python's " + "default lexicographic order." + ), + ) + ], + coordinates=[ + Coordinate( + coordinate_id=coordinate_id, + locus_id=locus_id, + facet_id=facet_id, + ) + ], + surface=VerificationSurface( + surface_id="surface:sorted-grocery-list", + subject_id=subject_id, + coordinate_ids=(coordinate_id,), + scope_statement=( + "One non-critical text-ordering coordinate; no physical, safety, " + "financial, medical, or operational claim." + ), + ), + mechanisms=[ + VerificationMechanism( + mechanism_id=CHECKER_MECHANISM_ID, + label="Lexicographic grocery-list checker", + version="experimental-0.1", + post_verified=False, + ) + ], + judgments=[ + CoordinateJudgment( + coordinate_id=coordinate_id, + status=CoordinateStatus.INDETERMINATE, + mechanism_ids=(CHECKER_MECHANISM_ID,), + limitations=("The checker has not been executed.",), + ) + ], + focus_coordinate_ids=(coordinate_id,), + ) + + +def build_registry() -> InteroperabilityComponentRegistry: + """Build the one-component experimental registry used by this example.""" + + descriptor = InteroperabilityComponentDescriptor( + component_id="component:lexicographic-check", + label="Lexicographic grocery-list checker", + kind=ComponentKind.VERIFIER, + lifecycle=ComponentLifecycle.EXPERIMENTAL, + implementation_ref=( + "examples/interoperability_planning/demo.py:check_lexicographic_order" + ), + accepted_schema_ids=("VSTD-2",), + native_system="Python sequence comparison", + native_objects=("sequence-of-grocery-item-strings",), + native_versions=("python-default-string-order",), + native_inputs=("grocery-item-string-sequence",), + native_outputs=("native-order-status",), + native_result_vocabulary=("INVALID_INPUT", "ORDERED", "OUT_OF_ORDER"), + mechanism_ids=(CHECKER_MECHANISM_ID,), + interaction_modes=(InteractionMode.STATIC,), + domain_tags=("demonstration", "non-critical", "text-processing"), + freshness_behavior="No result is retained or refreshed during planning.", + transformation_loss="Planning carries no native checker result.", + failure_behavior="Malformed native input would return INVALID_INPUT if executed.", + availability=ComponentAvailability.AVAILABLE, + claim_boundary=( + "Catalog matching identifies a plan candidate only; it does not run the " + "checker or establish ordering, validation, closure, or safety." + ), + ) + return InteroperabilityComponentRegistry( + registry_version="example:sorted-grocery-list:0.1", + components=(descriptor,), + ) + + +def build_demo() -> dict[str, Any]: + """Detect modeled holes and build a nonexecuting, registry-bound plan.""" + + geometry = build_geometry() + registry = build_registry() + analysis = analyze_verification_surface(geometry) + plan = plan_validation(analysis, registry) + exact_candidates = tuple( + candidate + for candidate in plan.candidates + if candidate.status is CandidateStatus.CANDIDATE + ) + unmatched = tuple( + candidate + for candidate in plan.candidates + if candidate.status is CandidateStatus.UNMATCHED + ) + return { + "example": "sorted-grocery-list", + "analysis": analysis.to_dict(), + "plan": plan.to_dict(), + "summary": { + "checker_invocations": _checker_invocations, + "exact_candidate_count": len(exact_candidates), + "execution_performed": plan.execution_performed, + "hole_count": len(analysis.holes), + "ordinary_closed": analysis.ordinary_closed, + "plan_only": plan.plan_only, + "self_closed": analysis.self_closed, + "unmatched_hole_count": len(unmatched), + }, + } + + +def render_demo() -> str: + """Return a deterministic JavaScript Object Notation rendering of the plan.""" + + return json.dumps(build_demo(), indent=2, sort_keys=True, allow_nan=False) + "\n" + + +if __name__ == "__main__": + print(render_demo(), end="") diff --git a/src/verifier/__init__.py b/src/verifier/__init__.py index 31ffced..e8de399 100644 --- a/src/verifier/__init__.py +++ b/src/verifier/__init__.py @@ -32,6 +32,18 @@ "VstdReceipt": ("verifier.core.receipt", "VstdReceipt"), "compute_canonical_digest": ("verifier.core.receipt", "compute_canonical_digest"), "ReproducibilityLevel": ("verifier.core.reproducibility", "ReproducibilityLevel"), + "PlatformComparisonResult": ( + "verifier.core.platform_comparison", + "PlatformComparisonResult", + ), + "PlatformComparisonStatus": ( + "verifier.core.platform_comparison", + "PlatformComparisonStatus", + ), + "compare_platform_run_receipts": ( + "verifier.core.platform_comparison", + "compare_platform_run_receipts", + ), "capture_run": ("verifier.core.run", "capture_run"), "validate_run_receipt": ("verifier.core.run", "validate_run_receipt"), "VerificationGeometry": ("verifier.core.geometry", "VerificationGeometry"), @@ -175,5 +187,10 @@ def __dir__() -> list[str]: VstdReceipt as VstdReceipt, compute_canonical_digest as compute_canonical_digest, ) + from verifier.core.platform_comparison import ( + PlatformComparisonResult as PlatformComparisonResult, + PlatformComparisonStatus as PlatformComparisonStatus, + compare_platform_run_receipts as compare_platform_run_receipts, + ) from verifier.core.reproducibility import ReproducibilityLevel as ReproducibilityLevel from verifier.core.run import capture_run as capture_run, validate_run_receipt as validate_run_receipt diff --git a/src/verifier/core/platform_comparison.py b/src/verifier/core/platform_comparison.py new file mode 100644 index 0000000..2389b8d --- /dev/null +++ b/src/verifier/core/platform_comparison.py @@ -0,0 +1,692 @@ +"""Terminology: JavaScript Object Notation (JSON); Secure Hash Algorithm 256-bit +(SHA-256); standard input/output (stdio); standard output (stdout); standard error +(stderr); Verifier Standard (VSTD). + +Bounded comparison of generic-run receipt results across operating systems. + +The declaration carried by each receipt names the operating systems and result +surfaces that are intended to be comparable. It is not evidence by itself. +This module returns ``PASS`` only when every declared operating system has one +valid receipt, the non-platform bindings agree, and the declared result +projections agree. Comparable disagreement is preserved as ``CONFLICTED``. +""" + +from __future__ import annotations + +import copy +from dataclasses import dataclass +from enum import Enum +import hashlib +import json +import unicodedata +from pathlib import Path +from typing import Any, Iterable, Mapping + +from verifier.core.receipt import ( + StrictJsonError, + canonical_json_dumps, + compute_canonical_digest, + strict_json_loads, +) +from verifier.core.run_validation import ( + _rebuild_stable_payload_from_dict, + _run_payload_errors, +) + + +PLATFORM_COMPARISON_MECHANISM = "VSTD-PLATFORM-COMPARISON-0.1" +_ALLOWED_RESULT_SURFACES = ( + "execution", + "declared_outputs", + "evaluator_claims", + "stdio", +) + + +def _implementation_digest() -> str: + digest = hashlib.sha256() + for path in ( + Path(__file__), + Path(__file__).with_name("run_validation.py"), + Path(__file__).with_name("receipt.py"), + ): + payload = path.read_bytes() + digest.update(path.name.encode("utf-8") + b"\0") + digest.update(len(payload).to_bytes(8, "big")) + digest.update(payload) + return "sha256:" + digest.hexdigest() + + +class PlatformComparisonStatus(str, Enum): + """Typed outcome of a bounded platform comparison.""" + + PASS = "PASS" + CONFLICTED = "CONFLICTED" + NOT_ESTABLISHED = "NOT_ESTABLISHED" + INVALID = "INVALID" + + +@dataclass(frozen=True) +class PlatformComparisonResult: + """Machine-readable diagnostic result; this object is not a VSTD receipt.""" + + status: PlatformComparisonStatus + reason: str + declaration: dict[str, Any] | None + required_platforms: tuple[str, ...] + observed_platforms: tuple[str, ...] + comparison_binding_digest: str | None + observations: tuple[dict[str, Any], ...] + differences: tuple[dict[str, Any], ...] + errors: tuple[str, ...] + + @property + def exit_code(self) -> int: + if self.status is PlatformComparisonStatus.PASS: + return 0 + if self.status in { + PlatformComparisonStatus.CONFLICTED, + PlatformComparisonStatus.INVALID, + }: + return 1 + return 2 + + def to_dict(self) -> dict[str, Any]: + return { + "report_kind": "platform_comparison_diagnostic", + "mechanism": { + "identifier": PLATFORM_COMPARISON_MECHANISM, + "implementation_sha256": _implementation_digest(), + "dependencies": ["python-stdlib"], + }, + "status": self.status.value, + "reason": self.reason, + "exit_code": self.exit_code, + "declaration": self.declaration, + "required_platforms": list(self.required_platforms), + "observed_platforms": list(self.observed_platforms), + "comparison_binding_digest": self.comparison_binding_digest, + "observations": list(self.observations), + "differences": list(self.differences), + "errors": list(self.errors), + "claim_boundary": ( + "PASS establishes only that the supplied canonically intact generic-run " + "receipts cover every declared operating system, share the compared " + "non-platform bindings, and record identical values on the declared " + "result surfaces. CONFLICTED establishes a recorded result disagreement " + "only after those comparability conditions pass." + ), + "limitations": [ + "The declaration does not prove that its named mechanism is compatible across operating systems.", + "The receipt platform value is an operating-system observation, not native-hardware or virtual-machine attestation.", + "The comparison does not establish semantic correctness, universal portability, actor independence, or behavior outside the supplied coordinates.", + "Receipt validation checks canonical integrity; this comparison does not independently rehash external artifacts or rerun recorded commands.", + ], + } + + +def _receipt_file(path: Path) -> Path: + return path / "receipt.json" if path.is_dir() else path + + +def _machine_family(value: object) -> str: + aliases = { + "amd64": "x86_64", + "x64": "x86_64", + "x86_64": "x86_64", + "aarch64": "arm64", + "arm64": "arm64", + } + normalized = str(value).lower() + return aliases.get(normalized, normalized) + + +def _platform_collision_key(value: str) -> str: + return unicodedata.normalize("NFKC", value).casefold() + + +def _load_receipt(path: Path) -> tuple[dict[str, Any] | None, list[str]]: + receipt_file = _receipt_file(path) + try: + payload = strict_json_loads(receipt_file.read_text(encoding="utf-8")) + except FileNotFoundError: + return None, [f"receipt file not found: {receipt_file}"] + except (OSError, UnicodeError, json.JSONDecodeError, StrictJsonError) as exc: + return None, [f"receipt is not readable JSON at {receipt_file}: {exc}"] + if not isinstance(payload, Mapping): + return None, [f"receipt root must be an object: {receipt_file}"] + data = dict(payload) + errors = _run_payload_errors(data) + if not errors: + recomputed = compute_canonical_digest(_rebuild_stable_payload_from_dict(data)) + if recomputed != data.get("canonical_digest"): + errors.append( + "canonical digest mismatch at " + f"{receipt_file}: recorded={data.get('canonical_digest')} " + f"recomputed={recomputed}" + ) + return data, [f"{receipt_file}: {error}" for error in errors] + + +def _parse_declaration( + receipt: Mapping[str, Any], +) -> tuple[dict[str, Any] | None, list[str], bool]: + surface = receipt.get("assessment_context", {}).get("refutation_surface", {}) + raw = surface.get("platform_comparability") if isinstance(surface, Mapping) else None + if raw is None: + return None, ["platform_comparability declaration is absent"], True + if not isinstance(raw, Mapping): + return None, ["platform_comparability must be an object"], False + expected = {"mechanism_id", "compatible_platforms", "result_surfaces"} + unexpected = sorted(set(raw) - expected) + missing = sorted(expected - set(raw)) + errors: list[str] = [] + if missing: + errors.append(f"platform_comparability missing fields: {', '.join(missing)}") + if unexpected: + errors.append( + f"platform_comparability has unexpected fields: {', '.join(unexpected)}" + ) + mechanism_id = raw.get("mechanism_id") + if ( + not isinstance(mechanism_id, str) + or not mechanism_id + or mechanism_id != mechanism_id.strip() + ): + errors.append( + "platform_comparability.mechanism_id must identify the compared subject mechanism" + ) + platforms = raw.get("compatible_platforms") + if ( + not isinstance(platforms, list) + or len(platforms) < 2 + or not all( + isinstance(item, str) and bool(item) and item == item.strip() + for item in platforms + ) + ): + errors.append( + "platform_comparability.compatible_platforms must contain at least two non-empty platform strings" + ) + normalized_platforms: list[str] = [] + else: + normalized_platforms = sorted(platforms) + if len(set(normalized_platforms)) != len(normalized_platforms): + errors.append( + "platform_comparability.compatible_platforms must not contain duplicates" + ) + collision_keys: dict[str, str] = {} + collisions: list[tuple[str, str]] = [] + for platform in platforms: + collision_key = _platform_collision_key(platform) + previous = collision_keys.get(collision_key) + if previous is not None and previous != platform: + collisions.append((previous, platform)) + else: + collision_keys[collision_key] = platform + if collisions: + rendered = ", ".join( + f"{first!r} and {second!r}" for first, second in collisions + ) + errors.append( + "platform_comparability.compatible_platforms values collide " + f"after Unicode normalization and casefold: {rendered}" + ) + surfaces = raw.get("result_surfaces") + if ( + not isinstance(surfaces, list) + or not surfaces + or not all(isinstance(item, str) for item in surfaces) + ): + errors.append( + "platform_comparability.result_surfaces must be a non-empty array of strings" + ) + normalized_surfaces: list[str] = [] + else: + unknown = sorted(set(surfaces) - set(_ALLOWED_RESULT_SURFACES)) + if unknown: + errors.append( + "platform_comparability.result_surfaces has unsupported values: " + + ", ".join(unknown) + ) + if len(set(surfaces)) != len(surfaces): + errors.append("platform_comparability.result_surfaces must not contain duplicates") + normalized_surfaces = [ + name for name in _ALLOWED_RESULT_SURFACES if name in surfaces + ] + if errors: + return None, errors, False + return { + "mechanism_id": mechanism_id, + "compatible_platforms": normalized_platforms, + "result_surfaces": normalized_surfaces, + }, [], False + + +def _normalize_source_hash_paths( + binding: dict[str, Any], platform: str +) -> None: + if _platform_collision_key(platform) != "windows": + return + source_state = binding.get("source_state_stable") + hashes = ( + source_state.get("source_file_hashes") + if isinstance(source_state, Mapping) + else None + ) + if not isinstance(hashes, Mapping): + raise ValueError("source_file_hashes must be an object") + normalized: dict[str, Any] = {} + original_paths: dict[str, str] = {} + for raw_path, digest in hashes.items(): + if not isinstance(raw_path, str): + raise ValueError("source_file_hashes keys must be strings") + normalized_path = raw_path.replace("\\", "/") + if normalized_path in normalized: + raise ValueError( + "source_file_hashes paths collide after Windows separator " + f"normalization: {original_paths[normalized_path]!r} and " + f"{raw_path!r}" + ) + normalized[normalized_path] = digest + original_paths[normalized_path] = raw_path + source_state["source_file_hashes"] = normalized + + +def _comparison_binding( + receipt: Mapping[str, Any], + result_surfaces: tuple[str, ...], + platform: str, +) -> dict[str, Any]: + binding = copy.deepcopy(_rebuild_stable_payload_from_dict(receipt)) + normalized_declaration, declaration_errors, _absent = _parse_declaration(receipt) + if normalized_declaration is None or declaration_errors: + raise ValueError("comparison binding requires a valid platform declaration") + binding["assessment_context"]["refutation_surface"][ + "platform_comparability" + ] = normalized_declaration + runtime = receipt["source_state"]["runtime"] + binding["platform_comparison_environment"] = { + "python_implementation": runtime.get("python_implementation"), + "machine_family": _machine_family(runtime.get("platform_machine")), + } + execution = binding["execution_stable"] + execution.pop("platform_system", None) + claims = binding["claims"] + + if "execution" in result_surfaces: + for name in ("exit_code", "outcome"): + execution.pop(name, None) + for name in ( + "execution_completed", + "output_digests_recorded", + "all_declared_artifacts_present", + ): + claims.pop(name, None) + if "declared_outputs" in result_surfaces: + binding["outputs"] = [ + {"path": output["path"], "role": output["role"]} + for output in receipt["outputs"] + ] + if "evaluator_claims" in result_surfaces: + claims["evaluator_claims"] = [ + { + "evaluator_name": evaluator["evaluator_name"], + "metric_name": evaluator["metric_name"], + "computed_by": evaluator["computed_by"], + "verified_independently": evaluator["verified_independently"], + } + for evaluator in receipt["claims"]["evaluator_claims"] + ] + if "stdio" in result_surfaces: + execution.pop("stdout_sha256", None) + execution.pop("stderr_sha256", None) + _normalize_source_hash_paths(binding, platform) + return binding + + +def _result_projection( + receipt: Mapping[str, Any], result_surfaces: tuple[str, ...] +) -> dict[str, Any]: + execution = receipt["execution"] + claims = receipt["claims"] + projection: dict[str, Any] = {} + if "execution" in result_surfaces: + projection["execution"] = { + "exit_code": execution["exit_code"], + "outcome": execution["outcome"], + "execution_completed": claims["execution_completed"], + "output_digests_recorded": claims["output_digests_recorded"], + "all_declared_artifacts_present": claims[ + "all_declared_artifacts_present" + ], + } + if "declared_outputs" in result_surfaces: + projection["declared_outputs"] = receipt["outputs"] + if "evaluator_claims" in result_surfaces: + projection["evaluator_claims"] = claims["evaluator_claims"] + if "stdio" in result_surfaces: + projection["stdio"] = { + "stdout_sha256": execution["stdout_sha256"], + "stderr_sha256": execution["stderr_sha256"], + } + return projection + + +def _differences( + reference: Any, observed: Any, *, path: str = "" +) -> list[dict[str, Any]]: + if isinstance(reference, Mapping) and isinstance(observed, Mapping): + differences: list[dict[str, Any]] = [] + for key in sorted(set(reference) | set(observed)): + child = f"{path}.{key}" if path else str(key) + if key not in reference: + differences.append( + {"path": child, "reference": None, "observed": observed[key]} + ) + elif key not in observed: + differences.append( + {"path": child, "reference": reference[key], "observed": None} + ) + else: + differences.extend( + _differences(reference[key], observed[key], path=child) + ) + return differences + if isinstance(reference, list) and isinstance(observed, list): + differences = [] + for index in range(max(len(reference), len(observed))): + child = f"{path}[{index}]" + if index >= len(reference): + differences.append( + {"path": child, "reference": None, "observed": observed[index]} + ) + elif index >= len(observed): + differences.append( + {"path": child, "reference": reference[index], "observed": None} + ) + else: + differences.extend( + _differences(reference[index], observed[index], path=child) + ) + return differences + if canonical_json_dumps(reference) != canonical_json_dumps(observed): + return [{"path": path, "reference": reference, "observed": observed}] + return [] + + +def _result( + status: PlatformComparisonStatus, + reason: str, + *, + declaration: dict[str, Any] | None = None, + required_platforms: Iterable[str] = (), + observed_platforms: Iterable[str] = (), + comparison_binding_digest: str | None = None, + observations: Iterable[dict[str, Any]] = (), + differences: Iterable[dict[str, Any]] = (), + errors: Iterable[str] = (), +) -> PlatformComparisonResult: + return PlatformComparisonResult( + status=status, + reason=reason, + declaration=declaration, + required_platforms=tuple(required_platforms), + observed_platforms=tuple(observed_platforms), + comparison_binding_digest=comparison_binding_digest, + observations=tuple(observations), + differences=tuple(differences), + errors=tuple(errors), + ) + + +def compare_platform_run_receipts( + receipts: Iterable[str | Path], +) -> PlatformComparisonResult: + """Compare declared result surfaces across canonically intact run receipts.""" + + loaded: list[tuple[Path, dict[str, Any]]] = [] + load_errors: list[str] = [] + for raw_path in receipts: + path = Path(raw_path) + receipt, errors = _load_receipt(path) + load_errors.extend(errors) + if receipt is not None: + loaded.append((path, receipt)) + if load_errors: + return _result( + PlatformComparisonStatus.INVALID, + "At least one supplied receipt is malformed or lacks canonical integrity.", + errors=load_errors, + ) + if not loaded: + return _result( + PlatformComparisonStatus.NOT_ESTABLISHED, + "No platform receipts were supplied.", + ) + + declarations: list[dict[str, Any]] = [] + declaration_errors: list[str] = [] + declaration_errors_are_all_absent = True + for path, receipt in loaded: + declaration, errors, absent = _parse_declaration(receipt) + if errors: + declaration_errors.extend(f"{_receipt_file(path)}: {error}" for error in errors) + declaration_errors_are_all_absent = ( + declaration_errors_are_all_absent and absent + ) + elif declaration is not None: + declarations.append(declaration) + if declaration_errors: + status = ( + PlatformComparisonStatus.NOT_ESTABLISHED + if declaration_errors_are_all_absent + else PlatformComparisonStatus.INVALID + ) + return _result( + status, + "Platform comparability is undeclared." + if status is PlatformComparisonStatus.NOT_ESTABLISHED + else "At least one platform-comparability declaration is malformed.", + errors=declaration_errors, + ) + + declaration = declarations[0] + if any(item != declaration for item in declarations[1:]): + return _result( + PlatformComparisonStatus.NOT_ESTABLISHED, + "The supplied receipts do not share one platform-comparability declaration.", + declaration=declaration, + errors=["platform_comparability declarations differ across receipts"], + ) + + required_platforms = tuple(declaration["compatible_platforms"]) + observed: dict[str, tuple[Path, dict[str, Any]]] = {} + set_errors: list[str] = [] + contradiction_errors: list[str] = [] + for path, receipt in loaded: + runtime = receipt["source_state"]["runtime"] + source_platform = runtime["platform_system"] + execution_platform = receipt["execution"]["platform_system"] + if source_platform != execution_platform: + contradiction_errors.append( + f"{_receipt_file(path)}: source_state.runtime.platform_system " + f"{source_platform!r} does not equal execution.platform_system " + f"{execution_platform!r}" + ) + continue + if runtime["python_version"] != receipt["execution"]["python_version"]: + contradiction_errors.append( + f"{_receipt_file(path)}: source_state.runtime.python_version " + "does not equal execution.python_version" + ) + continue + missing_environment = [ + name + for name in ("python_implementation", "platform_machine") + if not isinstance(runtime.get(name), str) or not runtime.get(name) + ] + if missing_environment: + set_errors.append( + f"{_receipt_file(path)}: comparison environment is missing " + + ", ".join(missing_environment) + ) + continue + if execution_platform in observed: + set_errors.append( + f"duplicate receipt for declared platform {execution_platform!r}" + ) + continue + observed[execution_platform] = (path, receipt) + if contradiction_errors: + return _result( + PlatformComparisonStatus.INVALID, + "At least one receipt contains internally contradictory execution observations.", + declaration=declaration, + required_platforms=required_platforms, + observed_platforms=sorted(observed), + errors=[*contradiction_errors, *set_errors], + ) + + missing = sorted(set(required_platforms) - set(observed)) + unexpected = sorted(set(observed) - set(required_platforms)) + if set_errors or missing or unexpected: + errors = list(set_errors) + if missing: + errors.append("missing declared platforms: " + ", ".join(missing)) + if unexpected: + errors.append("unexpected platforms: " + ", ".join(unexpected)) + return _result( + PlatformComparisonStatus.NOT_ESTABLISHED, + "The supplied receipt set does not contain exactly one receipt for every declared platform.", + declaration=declaration, + required_platforms=required_platforms, + observed_platforms=sorted(observed), + errors=errors, + ) + + surfaces = tuple(declaration["result_surfaces"]) + ordered = [(platform, *observed[platform]) for platform in required_platforms] + bindings: dict[str, dict[str, Any]] = {} + binding_errors: list[str] = [] + for platform, path, receipt in ordered: + try: + bindings[platform] = _comparison_binding( + receipt, surfaces, platform + ) + except ValueError as exc: + binding_errors.append(f"{_receipt_file(path)}: {exc}") + if binding_errors: + return _result( + PlatformComparisonStatus.INVALID, + "At least one receipt has ambiguous source-path identity.", + declaration=declaration, + required_platforms=required_platforms, + observed_platforms=required_platforms, + errors=binding_errors, + ) + binding_digests = { + platform: compute_canonical_digest(binding) + for platform, binding in bindings.items() + } + reference_platform = required_platforms[0] + binding_differences: list[dict[str, Any]] = [] + for platform in required_platforms[1:]: + for difference in _differences( + bindings[reference_platform], bindings[platform] + ): + binding_differences.append( + { + "kind": "comparison_binding", + "reference_platform": reference_platform, + "observed_platform": platform, + **difference, + } + ) + if binding_differences: + observations = [ + { + "platform": platform, + "platform_machine": receipt["source_state"]["runtime"].get( + "platform_machine" + ), + "machine_family": _machine_family( + receipt["source_state"]["runtime"].get("platform_machine") + ), + "receipt": str(_receipt_file(path)), + "receipt_id": receipt["receipt_id"], + "receipt_canonical_digest": receipt["canonical_digest"], + "comparison_binding_digest": binding_digests[platform], + "result_digest": None, + } + for platform, path, receipt in ordered + ] + return _result( + PlatformComparisonStatus.NOT_ESTABLISHED, + "Non-platform receipt bindings differ, so result disagreement would not isolate the operating-system dimension.", + declaration=declaration, + required_platforms=required_platforms, + observed_platforms=required_platforms, + observations=observations, + differences=binding_differences, + ) + + projections = { + platform: _result_projection(receipt, surfaces) + for platform, _path, receipt in ordered + } + result_digests = { + platform: compute_canonical_digest(projection) + for platform, projection in projections.items() + } + observations = [ + { + "platform": platform, + "platform_machine": receipt["source_state"]["runtime"].get( + "platform_machine" + ), + "machine_family": _machine_family( + receipt["source_state"]["runtime"].get("platform_machine") + ), + "receipt": str(_receipt_file(path)), + "receipt_id": receipt["receipt_id"], + "receipt_canonical_digest": receipt["canonical_digest"], + "comparison_binding_digest": binding_digests[platform], + "result_digest": result_digests[platform], + } + for platform, path, receipt in ordered + ] + result_differences: list[dict[str, Any]] = [] + for platform in required_platforms[1:]: + for difference in _differences( + projections[reference_platform], projections[platform] + ): + result_differences.append( + { + "kind": "declared_result", + "reference_platform": reference_platform, + "observed_platform": platform, + **difference, + } + ) + binding_digest = binding_digests[reference_platform] + if result_differences: + return _result( + PlatformComparisonStatus.CONFLICTED, + "Comparable receipts record different values on at least one declared result surface.", + declaration=declaration, + required_platforms=required_platforms, + observed_platforms=required_platforms, + comparison_binding_digest=binding_digest, + observations=observations, + differences=result_differences, + ) + return _result( + PlatformComparisonStatus.PASS, + "Every declared platform is represented once with matching non-platform bindings and recorded result projections.", + declaration=declaration, + required_platforms=required_platforms, + observed_platforms=required_platforms, + comparison_binding_digest=binding_digest, + observations=observations, + ) diff --git a/src/verifier/core/provenance.py b/src/verifier/core/provenance.py index 2d8b66d..400247f 100644 --- a/src/verifier/core/provenance.py +++ b/src/verifier/core/provenance.py @@ -216,7 +216,11 @@ def capture_provenance( file_hashes: dict[str, str] = {} for kf in key_files: if kf.exists() and kf.is_file(): - rel = str(kf.relative_to(repo_dir)) if kf.is_relative_to(repo_dir) else kf.name + rel = ( + kf.relative_to(repo_dir).as_posix() + if kf.is_relative_to(repo_dir) + else kf.name + ) file_hashes[rel] = sha256_file(kf) return ProvenanceRecord( diff --git a/src/verifier/core/receipt.py b/src/verifier/core/receipt.py index 48bab10..7178ffa 100644 --- a/src/verifier/core/receipt.py +++ b/src/verifier/core/receipt.py @@ -10,6 +10,7 @@ import hashlib import json +import math from dataclasses import dataclass from pathlib import Path from typing import Any, Mapping, Optional @@ -22,6 +23,43 @@ CLAIM_RECEIPT_KIND = "claim_mechanics" +class StrictJsonError(ValueError): + """A JSON document violates the strict VSTD-1 parsing boundary.""" + + +def _reject_duplicate_object_keys( + pairs: list[tuple[str, Any]], +) -> dict[str, Any]: + parsed: dict[str, Any] = {} + for key, value in pairs: + if key in parsed: + raise StrictJsonError(f"duplicate object key: {key!r}") + parsed[key] = value + return parsed + + +def _reject_non_finite_constant(value: str) -> Any: + raise StrictJsonError(f"non-finite number is not valid JSON: {value}") + + +def _parse_finite_float(value: str) -> float: + parsed = float(value) + if not math.isfinite(parsed): + raise StrictJsonError(f"JSON number is outside the finite float range: {value}") + return parsed + + +def strict_json_loads(payload: str) -> Any: + """Parse JSON while rejecting duplicate keys and non-finite numbers.""" + + return json.loads( + payload, + object_pairs_hook=_reject_duplicate_object_keys, + parse_constant=_reject_non_finite_constant, + parse_float=_parse_finite_float, + ) + + def canonical_json_dumps(payload: Any) -> str: """Deterministic JSON serialization. @@ -30,8 +68,15 @@ def canonical_json_dumps(payload: Any) -> str: 2. Compact separators (no trailing spaces: ',', ':'). 3. Floating point numbers formatted consistently. 4. Strings encoded in UTF-8. + 5. Non-finite numbers rejected rather than serialized as extensions. """ - return json.dumps(payload, sort_keys=True, separators=(",", ":"), ensure_ascii=True) + return json.dumps( + payload, + sort_keys=True, + separators=(",", ":"), + ensure_ascii=True, + allow_nan=False, + ) def compute_canonical_digest(stable_payload: Mapping[str, Any]) -> str: diff --git a/src/verifier/core/run_validation.py b/src/verifier/core/run_validation.py index 8cb2303..308f08a 100644 --- a/src/verifier/core/run_validation.py +++ b/src/verifier/core/run_validation.py @@ -10,7 +10,11 @@ import re from typing import Any, Mapping -from verifier.core.receipt import compute_canonical_digest +from verifier.core.receipt import ( + StrictJsonError, + compute_canonical_digest, + strict_json_loads, +) from verifier.core.run_support import ( RUN_RECEIPT_KIND, RUN_SCHEMA_VERSION, @@ -627,8 +631,8 @@ def validate_run_receipt(receipt_path_or_dir: Path) -> int: print(f"[FAIL] Receipt file not found: {receipt_file}") return 1 try: - data = json.loads(receipt_file.read_text(encoding="utf-8")) - except (OSError, json.JSONDecodeError) as exc: + data = strict_json_loads(receipt_file.read_text(encoding="utf-8")) + except (OSError, UnicodeError, json.JSONDecodeError, StrictJsonError) as exc: print(f"[FAIL] Receipt is not readable JSON: {exc}") return 1 if not isinstance(data, Mapping): diff --git a/src/verifier/data/graph_level.py b/src/verifier/data/graph_level.py index dd9e2e1..a253675 100644 --- a/src/verifier/data/graph_level.py +++ b/src/verifier/data/graph_level.py @@ -118,11 +118,11 @@ def graph_collection_binding_digest( class GraphEncodingError(RuntimeError): - """The encoding, the solver and the direct computation do not all agree. + """The Graph cannot be safely encoded or its independent checks disagree. - Carries the certificate the encoding actually supports, so a reader can see - which of the three is lying instead of being handed whichever branch this - module happened to prefer. + For cross-check divergence, this carries the certificate the encoding actually + supports, so a reader can see which of the three is lying instead of being + handed whichever branch this module happened to prefer. """ def __init__( @@ -139,6 +139,17 @@ def __init__( self.direct_result = direct_result +def _require_valid_graph_structure(graph: ProvenanceHypergraph) -> None: + """Refuse to encode a malformed Graph as a candidate or conformance result.""" + + errors = graph.validate_structure() + if errors: + raise GraphEncodingError( + "Graph profile computation requires a structurally valid provenance " + "graph: " + "; ".join(errors) + ) + + # -------------------------------------------------------------------------- # Obligations -- what a candidate Graph profile asks of a collection # -------------------------------------------------------------------------- @@ -536,6 +547,8 @@ def graph_level( an obligation met at ``N`` is met at every ``N' <= N`` -- so descending means a collection meeting its supplied ratings costs one solve rather than five. """ + _require_valid_graph_structure(graph) + if not collection.members: raise GraphEncodingError( f"{collection.collection_id} has no members, so every obligation is " @@ -613,6 +626,8 @@ def establish_graph_level( "artifact and transformation identifiers: " + ", ".join(identifier_overlap) ) + _require_valid_graph_structure(graph) + normalized_members = tuple(sorted(set(members))) closure = graph.ancestors(normalized_members) edges = { diff --git a/src/verifier/interoperability/__init__.py b/src/verifier/interoperability/__init__.py index 6da7ccd..3389a94 100644 --- a/src/verifier/interoperability/__init__.py +++ b/src/verifier/interoperability/__init__.py @@ -1 +1,49 @@ -"""Experimental adapters to adjacent verification and transparency standards.""" +"""Experimental interoperability catalog and surface-planning facade. + +This subpackage is outside the supported top-level :mod:`verifier` application +programming interface. Catalog membership and validation planning describe +nonexecuting candidates only; neither establishes a verifier result or closure. +""" + +from .catalog import ( + CatalogError, + ComponentAvailability, + ComponentKind, + ComponentLifecycle, + InteractionMode, + InteroperabilityComponentDescriptor, + InteroperabilityComponentRegistry, +) +from .control_surface import ( + CandidateStatus, + ControlSurfaceContext, + SurfaceAnalysis, + SurfaceAnalysisError, + SurfaceHole, + SurfaceHoleKind, + ValidationCandidate, + ValidationPlan, + analyze_verification_surface, + plan_validation, +) + + +__all__ = [ + "CandidateStatus", + "CatalogError", + "ComponentAvailability", + "ComponentKind", + "ComponentLifecycle", + "ControlSurfaceContext", + "InteractionMode", + "InteroperabilityComponentDescriptor", + "InteroperabilityComponentRegistry", + "SurfaceAnalysis", + "SurfaceAnalysisError", + "SurfaceHole", + "SurfaceHoleKind", + "ValidationCandidate", + "ValidationPlan", + "analyze_verification_surface", + "plan_validation", +] diff --git a/src/verifier/interoperability/catalog.py b/src/verifier/interoperability/catalog.py new file mode 100644 index 0000000..0c15da6 --- /dev/null +++ b/src/verifier/interoperability/catalog.py @@ -0,0 +1,435 @@ +"""Terminology: JavaScript Object Notation (JSON); Verifier Standard (VSTD). + +Domain-neutral component descriptors for VSTD interoperability planning. + +Registry membership describes an exact declared capability. It does not run a +component, establish availability, validate a native result, or grant assurance. +Domain tags are discovery metadata only and never participate in matching. +""" + +from __future__ import annotations + +import hashlib +import json +from dataclasses import dataclass +from enum import Enum +from typing import Any, Mapping, Optional + + +CATALOG_SCHEMA_VERSION = "VSTD-INTEROPERABILITY-CATALOG-1.0" + + +class CatalogError(ValueError): + """Raised when a component descriptor or registry is malformed.""" + + +class ComponentKind(str, Enum): + ADAPTER = "ADAPTER" + TRANSLATOR = "TRANSLATOR" + COMPARATOR = "COMPARATOR" + VERIFIER = "VERIFIER" + CONSTRAINT = "CONSTRAINT" + COLLECTOR = "COLLECTOR" + POLICY = "POLICY" + WORKFLOW = "WORKFLOW" + + +class ComponentLifecycle(str, Enum): + IMPLEMENTED = "IMPLEMENTED" + EXPERIMENTAL = "EXPERIMENTAL" + UNSUPPORTED = "UNSUPPORTED" + ABSENT = "ABSENT" + + +class ComponentAvailability(str, Enum): + AVAILABLE = "AVAILABLE" + NOT_CHECKED = "NOT_CHECKED" + UNAVAILABLE = "UNAVAILABLE" + + +class InteractionMode(str, Enum): + STATIC = "STATIC" + OFFLINE_REPLAY = "OFFLINE_REPLAY" + SIMULATION = "SIMULATION" + LIVE_READ_ONLY = "LIVE_READ_ONLY" + LIVE_MUTATING = "LIVE_MUTATING" + + +def _nonempty(value: Any, label: str) -> str: + if not isinstance(value, str) or not value or value != value.strip(): + raise CatalogError( + f"{label} must be a non-empty string without surrounding whitespace" + ) + return value + + +def _unique_strings(values: Any, label: str) -> tuple[str, ...]: + if not isinstance(values, (tuple, list)): + raise CatalogError(f"{label} must be an array of strings") + normalized = tuple(_nonempty(value, f"{label} item") for value in values) + if len(set(normalized)) != len(normalized): + raise CatalogError(f"{label} must not contain duplicates") + return tuple(sorted(normalized)) + + +def _interaction_modes(values: Any) -> tuple[InteractionMode, ...]: + if not isinstance(values, (tuple, list)): + raise CatalogError("interaction_modes must be an array") + try: + normalized = tuple(InteractionMode(value) for value in values) + except (TypeError, ValueError) as exc: + raise CatalogError(f"invalid interaction mode: {exc}") from exc + if len(set(normalized)) != len(normalized): + raise CatalogError("interaction_modes must not contain duplicates") + return tuple(sorted(normalized, key=lambda item: item.value)) + + +@dataclass(frozen=True) +class InteroperabilityComponentDescriptor: + """One component's exact, declared interoperability capability. + + ``domain_tags`` help callers group and present components. The exact-match + operation deliberately does not read them. ``supported_relations`` and + ``mechanism_ids`` declare a Cartesian product: every listed relation and + mechanism pair is supported. A component with a narrower capability must + use separate descriptors rather than rely on an implicit pairing. + """ + + component_id: str + label: str + kind: ComponentKind + lifecycle: ComponentLifecycle + implementation_ref: str + accepted_schema_ids: tuple[str, ...] + verifier_family_ids: tuple[str, ...] = () + native_system: str = "" + native_objects: tuple[str, ...] = () + native_versions: tuple[str, ...] = () + native_inputs: tuple[str, ...] = () + native_outputs: tuple[str, ...] = () + native_result_vocabulary: tuple[str, ...] = () + emitted_schema_ids: tuple[str, ...] = () + supported_relations: tuple[str, ...] = () + mechanism_ids: tuple[str, ...] = () + interaction_modes: tuple[InteractionMode, ...] = (InteractionMode.STATIC,) + domain_tags: tuple[str, ...] = () + optional_dependencies: tuple[str, ...] = () + execution_prerequisites: tuple[str, ...] = () + trust_roots: tuple[str, ...] = () + freshness_behavior: str = "" + transformation_loss: str = "" + failure_behavior: str = "" + availability: ComponentAvailability = ComponentAvailability.NOT_CHECKED + claim_boundary: str = "" + + def __post_init__(self) -> None: + object.__setattr__(self, "component_id", _nonempty(self.component_id, "component_id")) + object.__setattr__(self, "label", _nonempty(self.label, "label")) + object.__setattr__( + self, "implementation_ref", _nonempty(self.implementation_ref, "implementation_ref") + ) + try: + object.__setattr__(self, "kind", ComponentKind(self.kind)) + object.__setattr__(self, "lifecycle", ComponentLifecycle(self.lifecycle)) + object.__setattr__(self, "availability", ComponentAvailability(self.availability)) + except (TypeError, ValueError) as exc: + raise CatalogError(str(exc)) from exc + object.__setattr__( + self, + "accepted_schema_ids", + _unique_strings(self.accepted_schema_ids, "accepted_schema_ids"), + ) + if not self.accepted_schema_ids: + raise CatalogError("accepted_schema_ids must not be empty") + object.__setattr__( + self, + "emitted_schema_ids", + _unique_strings(self.emitted_schema_ids, "emitted_schema_ids"), + ) + object.__setattr__( + self, + "supported_relations", + _unique_strings(self.supported_relations, "supported_relations"), + ) + object.__setattr__( + self, "mechanism_ids", _unique_strings(self.mechanism_ids, "mechanism_ids") + ) + if not self.mechanism_ids: + raise CatalogError("a component must declare at least one mechanism identifier") + object.__setattr__(self, "interaction_modes", _interaction_modes(self.interaction_modes)) + if not self.interaction_modes: + raise CatalogError("interaction_modes must not be empty") + for field_name in ( + "verifier_family_ids", + "native_objects", + "native_versions", + "native_inputs", + "native_outputs", + "native_result_vocabulary", + "domain_tags", + "optional_dependencies", + "execution_prerequisites", + "trust_roots", + ): + object.__setattr__( + self, field_name, _unique_strings(getattr(self, field_name), field_name) + ) + for field_name in ( + "native_system", + "freshness_behavior", + "transformation_loss", + "failure_behavior", + "claim_boundary", + ): + if not isinstance(getattr(self, field_name), str): + raise CatalogError(f"{field_name} must be a string") + + def matches_exact( + self, + *, + schema_id: str, + interaction_mode: InteractionMode, + relation_id: Optional[str] = None, + mechanism_id: Optional[str] = None, + ) -> bool: + """Return whether every supplied semantic coordinate matches exactly. + + At least a relation or mechanism coordinate is required. This prevents + the registry from treating a shared schema and interaction mode as a + sufficient capability match. When both are supplied, they name one + exact coordinate in the descriptor's declared relation-by-mechanism + Cartesian product; matching either coordinate alone is insufficient. + """ + + try: + mode = InteractionMode(interaction_mode) + except (TypeError, ValueError): + return False + if relation_id is None and mechanism_id is None: + return False + if schema_id not in self.accepted_schema_ids or mode not in self.interaction_modes: + return False + if relation_id is not None and relation_id not in self.supported_relations: + return False + if mechanism_id is not None and mechanism_id not in self.mechanism_ids: + return False + return True + + def to_dict(self) -> dict[str, Any]: + return { + "component_id": self.component_id, + "label": self.label, + "kind": self.kind.value, + "lifecycle": self.lifecycle.value, + "implementation_ref": self.implementation_ref, + "accepted_schema_ids": list(self.accepted_schema_ids), + "verifier_family_ids": list(self.verifier_family_ids), + "native_system": self.native_system, + "native_objects": list(self.native_objects), + "native_versions": list(self.native_versions), + "native_inputs": list(self.native_inputs), + "native_outputs": list(self.native_outputs), + "native_result_vocabulary": list(self.native_result_vocabulary), + "emitted_schema_ids": list(self.emitted_schema_ids), + "supported_relations": list(self.supported_relations), + "mechanism_ids": list(self.mechanism_ids), + "interaction_modes": [item.value for item in self.interaction_modes], + "domain_tags": list(self.domain_tags), + "optional_dependencies": list(self.optional_dependencies), + "execution_prerequisites": list(self.execution_prerequisites), + "trust_roots": list(self.trust_roots), + "freshness_behavior": self.freshness_behavior, + "transformation_loss": self.transformation_loss, + "failure_behavior": self.failure_behavior, + "availability": self.availability.value, + "claim_boundary": self.claim_boundary, + } + + @classmethod + def from_dict(cls, value: Mapping[str, Any]) -> "InteroperabilityComponentDescriptor": + if not isinstance(value, Mapping): + raise CatalogError("component descriptor must be an object") + expected = { + "component_id", + "label", + "kind", + "lifecycle", + "implementation_ref", + "accepted_schema_ids", + "verifier_family_ids", + "native_system", + "native_objects", + "native_versions", + "native_inputs", + "native_outputs", + "native_result_vocabulary", + "emitted_schema_ids", + "supported_relations", + "mechanism_ids", + "interaction_modes", + "domain_tags", + "optional_dependencies", + "execution_prerequisites", + "trust_roots", + "freshness_behavior", + "transformation_loss", + "failure_behavior", + "availability", + "claim_boundary", + } + actual = set(value) + if actual != expected: + raise CatalogError( + "component descriptor keys mismatch; " + f"missing={sorted(expected - actual)}, extra={sorted(actual - expected)}" + ) + return cls( + component_id=value["component_id"], + label=value["label"], + kind=value["kind"], + lifecycle=value["lifecycle"], + implementation_ref=value["implementation_ref"], + accepted_schema_ids=value["accepted_schema_ids"], + verifier_family_ids=value["verifier_family_ids"], + native_system=value["native_system"], + native_objects=value["native_objects"], + native_versions=value["native_versions"], + native_inputs=value["native_inputs"], + native_outputs=value["native_outputs"], + native_result_vocabulary=value["native_result_vocabulary"], + emitted_schema_ids=value["emitted_schema_ids"], + supported_relations=value["supported_relations"], + mechanism_ids=value["mechanism_ids"], + interaction_modes=value["interaction_modes"], + domain_tags=value["domain_tags"], + optional_dependencies=value["optional_dependencies"], + execution_prerequisites=value["execution_prerequisites"], + trust_roots=value["trust_roots"], + freshness_behavior=value["freshness_behavior"], + transformation_loss=value["transformation_loss"], + failure_behavior=value["failure_behavior"], + availability=value["availability"], + claim_boundary=value["claim_boundary"], + ) + + +@dataclass(frozen=True) +class InteroperabilityComponentRegistry: + """Immutable, deterministically ordered component catalog.""" + + registry_version: str + components: tuple[InteroperabilityComponentDescriptor, ...] + schema_version: str = CATALOG_SCHEMA_VERSION + + def __post_init__(self) -> None: + object.__setattr__( + self, "registry_version", _nonempty(self.registry_version, "registry_version") + ) + if self.schema_version != CATALOG_SCHEMA_VERSION: + raise CatalogError(f"schema_version must be {CATALOG_SCHEMA_VERSION!r}") + if not isinstance(self.components, (tuple, list)): + raise CatalogError("components must be an array of component descriptors") + components = tuple(self.components) + if not all(isinstance(item, InteroperabilityComponentDescriptor) for item in components): + raise CatalogError("components must contain component descriptors") + identifiers = [item.component_id for item in components] + if len(set(identifiers)) != len(identifiers): + duplicates = sorted( + identifier for identifier in set(identifiers) if identifiers.count(identifier) > 1 + ) + raise CatalogError(f"duplicate component identifiers: {', '.join(duplicates)}") + object.__setattr__( + self, "components", tuple(sorted(components, key=lambda item: item.component_id)) + ) + + def list(self) -> tuple[InteroperabilityComponentDescriptor, ...]: + return self.components + + def get(self, component_id: str) -> InteroperabilityComponentDescriptor: + component_id = _nonempty(component_id, "component_id") + for component in self.components: + if component.component_id == component_id: + return component + raise CatalogError(f"unknown interoperability component {component_id!r}") + + def match_exact( + self, + *, + schema_id: str, + interaction_mode: InteractionMode, + relation_id: Optional[str] = None, + mechanism_id: Optional[str] = None, + ) -> tuple[InteroperabilityComponentDescriptor, ...]: + """Return every exact match in stable component-identifier order.""" + + return tuple( + component + for component in self.components + if component.matches_exact( + schema_id=schema_id, + interaction_mode=interaction_mode, + relation_id=relation_id, + mechanism_id=mechanism_id, + ) + ) + + def to_dict(self) -> dict[str, Any]: + return { + "schema_version": self.schema_version, + "registry_version": self.registry_version, + "components": [component.to_dict() for component in self.components], + } + + def canonical_json_bytes(self) -> bytes: + return json.dumps( + self.to_dict(), + sort_keys=True, + separators=(",", ":"), + ensure_ascii=True, + allow_nan=False, + ).encode("utf-8") + + def canonical_digest(self) -> str: + return hashlib.sha256(self.canonical_json_bytes()).hexdigest() + + @classmethod + def from_dict(cls, value: Mapping[str, Any]) -> "InteroperabilityComponentRegistry": + if not isinstance(value, Mapping): + raise CatalogError("component registry must be an object") + expected = {"schema_version", "registry_version", "components"} + actual = set(value) + if actual != expected: + raise CatalogError( + "component registry keys mismatch; " + f"missing={sorted(expected - actual)}, extra={sorted(actual - expected)}" + ) + components = value["components"] + if not isinstance(components, list) or not all( + isinstance(item, Mapping) for item in components + ): + raise CatalogError("components must be an array of objects") + return cls( + schema_version=value["schema_version"], + registry_version=value["registry_version"], + components=tuple( + InteroperabilityComponentDescriptor.from_dict(item) for item in components + ), + ) + + +# ``InteroperabilityCatalog`` is the presentation name used by the release plan; +# both names identify the same immutable registry contract. +InteroperabilityCatalog = InteroperabilityComponentRegistry + + +__all__ = [ + "CATALOG_SCHEMA_VERSION", + "CatalogError", + "ComponentAvailability", + "ComponentKind", + "ComponentLifecycle", + "InteractionMode", + "InteroperabilityCatalog", + "InteroperabilityComponentDescriptor", + "InteroperabilityComponentRegistry", +] diff --git a/src/verifier/interoperability/control_surface.py b/src/verifier/interoperability/control_surface.py new file mode 100644 index 0000000..edc6e1b --- /dev/null +++ b/src/verifier/interoperability/control_surface.py @@ -0,0 +1,1054 @@ +"""Terminology: JavaScript Object Notation (JSON); Verifier Standard (VSTD). + +Stable analysis of the declared VSTD-2 control surface and experimental, +nonexecuting candidate planning. + +The analyzer derives diagnostics only from geometry already represented by the +caller. It does not infer an expected profile, omitted ontology, or real-world +completeness. A catalog match is a candidate association, not a verification +result and not authority to execute a component. +""" + +from __future__ import annotations + +import hashlib +import json +from dataclasses import dataclass, field +from enum import Enum +from typing import Any, Optional + +from verifier.core.geometry import ( + CoordinateStatus, + GEOMETRY_SCHEMA_VERSION, + ResidualDisposition, + ValenceStatus, + VerificationGeometry, +) + +from .catalog import ( + ComponentAvailability, + ComponentLifecycle, + InteractionMode, + InteroperabilityComponentDescriptor, + InteroperabilityComponentRegistry, +) + + +ANALYSIS_SCHEMA_VERSION = "VSTD-SURFACE-ANALYSIS-1.0" +PLAN_SCHEMA_VERSION = "VSTD-VALIDATION-PLAN-EXPERIMENTAL-0.1" +MODELED_SURFACE_SCOPE = "MODELED_SURFACE_ONLY" +ANALYSIS_INPUT_MODE = "TYPED_VERIFICATION_GEOMETRY_ONLY" +STRICT_WIRE_LOADING_STATUS = "UNSUPPORTED" + +ANALYSIS_CLAIM_BOUNDARY = ( + "Derived only from the supplied VSTD-2 geometry. It does not establish that the " + "declared surface, coordinates, ontology, or evidence are complete." +) +PLAN_CLAIM_BOUNDARY = ( + "Exact catalog matches are nonexecuting candidates. They do not establish " + "availability, validity, assurance, authority, or closure." +) +EXECUTION_PREREQUISITES = ( + "BOUND_PROPOSITION", + "MECHANISM_DIGEST", + "EVIDENCE_BYTES", + "TRUST_ROOTS", + "RESOURCE_BOUNDS", +) + + +class SurfaceAnalysisError(ValueError): + """Raised when an analysis or candidate-plan object is malformed.""" + + +class SurfaceHoleKind(str, Enum): + MISSING_JUDGMENT = "MISSING_JUDGMENT" + COORDINATE_STATUS = "COORDINATE_STATUS" + RESIDUAL = "RESIDUAL" + VALENCE = "VALENCE" + HORIZON = "HORIZON" + MECHANISM = "MECHANISM" + VERIFICATION_ORDER = "VERIFICATION_ORDER" + SELF_CLOSURE_REQUIREMENT = "SELF_CLOSURE_REQUIREMENT" + + +class CandidateStatus(str, Enum): + CANDIDATE = "CANDIDATE" + BLOCKED = "BLOCKED" + UNMATCHED = "UNMATCHED" + + +def _nonempty(value: Any, label: str) -> str: + if not isinstance(value, str) or not value or value != value.strip(): + raise SurfaceAnalysisError( + f"{label} must be a non-empty string without surrounding whitespace" + ) + return value + + +def _unique_strings(values: Any, label: str) -> tuple[str, ...]: + if not isinstance(values, (tuple, list)): + raise SurfaceAnalysisError(f"{label} must be an array of strings") + normalized = tuple(_nonempty(value, f"{label} item") for value in values) + if len(set(normalized)) != len(normalized): + raise SurfaceAnalysisError(f"{label} must not contain duplicates") + return tuple(sorted(normalized)) + + +def _canonical_json_bytes(value: Any) -> bytes: + return json.dumps( + value, + sort_keys=True, + separators=(",", ":"), + ensure_ascii=True, + allow_nan=False, + ).encode("utf-8") + + +def _stable_identifier(prefix: str, value: Any) -> str: + return f"{prefix}:{hashlib.sha256(_canonical_json_bytes(value)).hexdigest()}" + + +@dataclass(frozen=True) +class ControlSurfaceContext: + """External planning context; only schema and interaction mode affect matches.""" + + schema_id: str = GEOMETRY_SCHEMA_VERSION + interaction_mode: InteractionMode = InteractionMode.STATIC + domain_tags: tuple[str, ...] = () + operating_regime: str = "" + consequence_profiles: tuple[str, ...] = () + authority_requirements: tuple[str, ...] = () + + def __post_init__(self) -> None: + object.__setattr__(self, "schema_id", _nonempty(self.schema_id, "schema_id")) + try: + object.__setattr__(self, "interaction_mode", InteractionMode(self.interaction_mode)) + except (TypeError, ValueError) as exc: + raise SurfaceAnalysisError(str(exc)) from exc + for field_name in ( + "domain_tags", + "consequence_profiles", + "authority_requirements", + ): + object.__setattr__( + self, field_name, _unique_strings(getattr(self, field_name), field_name) + ) + if not isinstance(self.operating_regime, str): + raise SurfaceAnalysisError("operating_regime must be a string") + if self.operating_regime and self.operating_regime != self.operating_regime.strip(): + raise SurfaceAnalysisError( + "operating_regime must not contain surrounding whitespace" + ) + + def to_dict(self) -> dict[str, Any]: + return { + "schema_id": self.schema_id, + "interaction_mode": self.interaction_mode.value, + "domain_tags": list(self.domain_tags), + "operating_regime": self.operating_regime, + "consequence_profiles": list(self.consequence_profiles), + "authority_requirements": list(self.authority_requirements), + } + + +@dataclass(frozen=True) +class SurfaceHole: + """One structured diagnostic derived from an existing geometry blocker.""" + + hole_id: str + kind: SurfaceHoleKind + source_kind: str + source_id: str + native_status: str + description: str + blocks_ordinary_closure: bool + blocks_self_closure: bool + schema_id: str + interaction_mode: InteractionMode + required_relations: tuple[str, ...] = () + mechanism_ids: tuple[str, ...] = () + + def __post_init__(self) -> None: + for field_name in ( + "hole_id", + "source_kind", + "source_id", + "native_status", + "description", + "schema_id", + ): + object.__setattr__( + self, field_name, _nonempty(getattr(self, field_name), field_name) + ) + try: + object.__setattr__(self, "kind", SurfaceHoleKind(self.kind)) + object.__setattr__(self, "interaction_mode", InteractionMode(self.interaction_mode)) + except (TypeError, ValueError) as exc: + raise SurfaceAnalysisError(str(exc)) from exc + if type(self.blocks_ordinary_closure) is not bool: + raise SurfaceAnalysisError("blocks_ordinary_closure must be a boolean") + if type(self.blocks_self_closure) is not bool: + raise SurfaceAnalysisError("blocks_self_closure must be a boolean") + object.__setattr__( + self, + "required_relations", + _unique_strings(self.required_relations, "required_relations"), + ) + object.__setattr__( + self, "mechanism_ids", _unique_strings(self.mechanism_ids, "mechanism_ids") + ) + + @property + def native_state(self) -> str: + """Compatibility spelling for callers that describe statuses as states.""" + + return self.native_status + + def to_dict(self) -> dict[str, Any]: + return { + "hole_id": self.hole_id, + "kind": self.kind.value, + "source_kind": self.source_kind, + "source_id": self.source_id, + "native_status": self.native_status, + "description": self.description, + "blocks_ordinary_closure": self.blocks_ordinary_closure, + "blocks_self_closure": self.blocks_self_closure, + "schema_id": self.schema_id, + "interaction_mode": self.interaction_mode.value, + "required_relations": list(self.required_relations), + "mechanism_ids": list(self.mechanism_ids), + } + + +@dataclass(frozen=True) +class SurfaceAnalysis: + """Deterministic report over only the supplied, declared geometry.""" + + geometry_id: str + geometry_digest: str + context: ControlSurfaceContext + validity_errors: tuple[str, ...] + ordinary_closed: bool + self_closed: bool + ordinary_blockers: tuple[str, ...] + self_closure_blockers: tuple[str, ...] + holes: tuple[SurfaceHole, ...] + scope: str = MODELED_SURFACE_SCOPE + input_mode: str = ANALYSIS_INPUT_MODE + strict_wire_loading_status: str = STRICT_WIRE_LOADING_STATUS + claim_boundary: str = ANALYSIS_CLAIM_BOUNDARY + schema_version: str = ANALYSIS_SCHEMA_VERSION + + def __post_init__(self) -> None: + _nonempty(self.geometry_id, "geometry_id") + _nonempty(self.geometry_digest, "geometry_digest") + if not isinstance(self.context, ControlSurfaceContext): + raise SurfaceAnalysisError("context must be a ControlSurfaceContext") + if self.scope != MODELED_SURFACE_SCOPE: + raise SurfaceAnalysisError(f"scope must be {MODELED_SURFACE_SCOPE!r}") + if self.input_mode != ANALYSIS_INPUT_MODE: + raise SurfaceAnalysisError(f"input_mode must be {ANALYSIS_INPUT_MODE!r}") + if self.strict_wire_loading_status != STRICT_WIRE_LOADING_STATUS: + raise SurfaceAnalysisError( + "strict_wire_loading_status must remain " + f"{STRICT_WIRE_LOADING_STATUS!r}" + ) + if self.schema_version != ANALYSIS_SCHEMA_VERSION: + raise SurfaceAnalysisError( + f"schema_version must be {ANALYSIS_SCHEMA_VERSION!r}" + ) + if self.claim_boundary != ANALYSIS_CLAIM_BOUNDARY: + raise SurfaceAnalysisError( + "claim_boundary is fixed and cannot be weakened or replaced" + ) + if type(self.ordinary_closed) is not bool: + raise SurfaceAnalysisError("ordinary_closed must be a boolean") + if type(self.self_closed) is not bool: + raise SurfaceAnalysisError("self_closed must be a boolean") + for field_name in ( + "validity_errors", + "ordinary_blockers", + "self_closure_blockers", + ): + values = getattr(self, field_name) + if not isinstance(values, (tuple, list)) or not all( + isinstance(item, str) for item in values + ): + raise SurfaceAnalysisError(f"{field_name} must be an array of strings") + object.__setattr__(self, field_name, tuple(values)) + if not isinstance(self.holes, (tuple, list)): + raise SurfaceAnalysisError("holes must be an array of SurfaceHole records") + holes = tuple(self.holes) + if not all(isinstance(item, SurfaceHole) for item in holes): + raise SurfaceAnalysisError("holes must contain SurfaceHole records") + hole_ids = [item.hole_id for item in holes] + if len(set(hole_ids)) != len(hole_ids): + raise SurfaceAnalysisError("holes must have unique identifiers") + object.__setattr__( + self, + "holes", + tuple( + sorted( + holes, + key=lambda item: ( + item.kind.value, + item.source_kind, + item.source_id, + item.native_status, + item.hole_id, + ), + ) + ), + ) + + def to_dict(self) -> dict[str, Any]: + return { + "schema_version": self.schema_version, + "geometry_id": self.geometry_id, + "geometry_digest": self.geometry_digest, + "scope": self.scope, + "input_mode": self.input_mode, + "strict_wire_loading_status": self.strict_wire_loading_status, + "context": self.context.to_dict(), + "validity_errors": list(self.validity_errors), + "ordinary_closed": self.ordinary_closed, + "self_closed": self.self_closed, + "ordinary_blockers": list(self.ordinary_blockers), + "self_closure_blockers": list(self.self_closure_blockers), + "holes": [hole.to_dict() for hole in self.holes], + "claim_boundary": self.claim_boundary, + } + + def canonical_json_bytes(self) -> bytes: + return _canonical_json_bytes(self.to_dict()) + + +def _make_hole( + *, + kind: SurfaceHoleKind, + source_kind: str, + source_id: str, + native_status: str, + description: str, + ordinary: bool, + self_closure: bool, + context: ControlSurfaceContext, + required_relations: tuple[str, ...] = (), + mechanism_ids: tuple[str, ...] = (), +) -> SurfaceHole: + relations = tuple(sorted(set(required_relations))) + mechanisms = tuple(sorted(set(mechanism_ids))) + identifier_material = { + "kind": kind.value, + "source_kind": source_kind, + "source_id": source_id, + "native_status": native_status, + "schema_id": context.schema_id, + "interaction_mode": context.interaction_mode.value, + "required_relations": list(relations), + "mechanism_ids": list(mechanisms), + } + return SurfaceHole( + hole_id=_stable_identifier("surface-hole", identifier_material), + kind=kind, + source_kind=source_kind, + source_id=source_id, + native_status=native_status, + description=description, + blocks_ordinary_closure=ordinary, + blocks_self_closure=self_closure, + schema_id=context.schema_id, + interaction_mode=context.interaction_mode, + required_relations=relations, + mechanism_ids=mechanisms, + ) + + +def analyze_verification_surface( + geometry: VerificationGeometry, + context: Optional[ControlSurfaceContext] = None, +) -> SurfaceAnalysis: + """Derive structured holes from the supplied VSTD-2 geometry only.""" + + if not isinstance(geometry, VerificationGeometry): + raise SurfaceAnalysisError("geometry must be a VerificationGeometry") + if context is None: + context = ControlSurfaceContext(schema_id=geometry.schema_version) + elif not isinstance(context, ControlSurfaceContext): + raise SurfaceAnalysisError("context must be a ControlSurfaceContext") + elif context.schema_id != geometry.schema_version: + raise SurfaceAnalysisError( + "context schema_id must exactly equal the typed geometry schema_version" + ) + + try: + validity_errors = tuple(geometry.validate()) + except (AttributeError, KeyError, TypeError, ValueError) as exc: + raise SurfaceAnalysisError( + f"verification geometry validation failed: {exc}" + ) from exc + if validity_errors: + raise SurfaceAnalysisError( + "verification geometry is structurally invalid: " + + "; ".join(validity_errors) + ) + try: + assessment = geometry.assess_closure() + except (AttributeError, KeyError, TypeError, ValueError) as exc: + raise SurfaceAnalysisError( + f"verification geometry closure assessment failed: {exc}" + ) from exc + judgments: dict[str, Any] = {} + for judgment in geometry.judgments: + judgments.setdefault(judgment.coordinate_id, judgment) + surface_coordinate_ids = set(geometry.surface.coordinate_ids) + holes: dict[tuple[str, str, str, str], SurfaceHole] = {} + + def add(hole: SurfaceHole) -> None: + key = ( + hole.kind.value, + hole.source_kind, + hole.source_id, + hole.native_status, + ) + existing = holes.get(key) + if existing is None: + holes[key] = hole + return + holes[key] = _make_hole( + kind=hole.kind, + source_kind=hole.source_kind, + source_id=hole.source_id, + native_status=hole.native_status, + description=existing.description, + ordinary=( + existing.blocks_ordinary_closure or hole.blocks_ordinary_closure + ), + self_closure=(existing.blocks_self_closure or hole.blocks_self_closure), + context=context, + required_relations=tuple( + set(existing.required_relations) | set(hole.required_relations) + ), + mechanism_ids=tuple(set(existing.mechanism_ids) | set(hole.mechanism_ids)), + ) + + def add_coordinate_hole( + coordinate_id: str, *, ordinary: bool, layer_mechanisms: tuple[str, ...] = () + ) -> None: + judgment = judgments.get(coordinate_id) + if judgment is None: + add( + _make_hole( + kind=SurfaceHoleKind.MISSING_JUDGMENT, + source_kind="COORDINATE", + source_id=coordinate_id, + native_status="MISSING_JUDGMENT", + description=f"Coordinate {coordinate_id!r} has no current judgment.", + ordinary=ordinary, + self_closure=True, + context=context, + mechanism_ids=layer_mechanisms, + ) + ) + elif judgment.status is not CoordinateStatus.VERIFIED: + add( + _make_hole( + kind=SurfaceHoleKind.COORDINATE_STATUS, + source_kind="COORDINATE", + source_id=coordinate_id, + native_status=judgment.status.value, + description=( + f"Coordinate {coordinate_id!r} retains native status " + f"{judgment.status.value}; it is not VERIFIED." + ), + ordinary=ordinary, + self_closure=True, + context=context, + mechanism_ids=tuple(judgment.mechanism_ids) or layer_mechanisms, + ) + ) + + for coordinate_id in geometry.surface.coordinate_ids: + add_coordinate_hole(coordinate_id, ordinary=True) + + relations_by_horizon: dict[str, set[str]] = {} + mechanisms_by_horizon: dict[str, set[str]] = {} + relations_by_location: dict[tuple[str, str], set[str]] = {} + for valence in geometry.valences: + relations_by_location.setdefault( + (valence.source_kind, valence.source_id), set() + ).add(valence.required_relation) + if valence.horizon_id: + relations_by_horizon.setdefault(valence.horizon_id, set()).add( + valence.required_relation + ) + for mechanism in geometry.mechanisms: + if mechanism.boundary_horizon_id: + mechanisms_by_horizon.setdefault(mechanism.boundary_horizon_id, set()).add( + mechanism.mechanism_id + ) + for verification_order in geometry.verification_layers: + if verification_order.horizon_id: + mechanisms_by_horizon.setdefault(verification_order.horizon_id, set()).update( + verification_order.mechanism_ids + ) + + for residual in geometry.residuals: + if not residual.material or residual.disposition is ResidualDisposition.RESOLVED: + continue + relations: set[str] = set() + mechanisms: set[str] = set() + for source_kind, source_id in ( + ("LOCUS", residual.locus_id), + ("COORDINATE", residual.coordinate_id), + ("SEAM", residual.seam_id), + ): + if source_id: + relations.update(relations_by_location.get((source_kind, source_id), set())) + if residual.coordinate_id and residual.coordinate_id in judgments: + mechanisms.update(judgments[residual.coordinate_id].mechanism_ids) + if residual.horizon_id: + relations.update(relations_by_horizon.get(residual.horizon_id, set())) + mechanisms.update(mechanisms_by_horizon.get(residual.horizon_id, set())) + add( + _make_hole( + kind=SurfaceHoleKind.RESIDUAL, + source_kind="RESIDUAL", + source_id=residual.residual_id, + native_status=residual.disposition.value, + description=residual.description, + ordinary=residual.disposition + not in {ResidualDisposition.RESOLVED, ResidualDisposition.HORIZON}, + self_closure=True, + context=context, + required_relations=tuple(relations), + mechanism_ids=tuple(mechanisms), + ) + ) + + for valence in geometry.valences: + if valence.status is ValenceStatus.DISCHARGED: + continue + add( + _make_hole( + kind=SurfaceHoleKind.VALENCE, + source_kind="VALENCE", + source_id=valence.valence_id, + native_status=valence.status.value, + description=valence.description, + ordinary=False, + self_closure=True, + context=context, + required_relations=(valence.required_relation,), + ) + ) + + for horizon in geometry.horizons: + add( + _make_hole( + kind=SurfaceHoleKind.HORIZON, + source_kind="HORIZON", + source_id=horizon.horizon_id, + native_status=horizon.kind.value, + description=horizon.description, + ordinary=False, + self_closure=True, + context=context, + required_relations=tuple( + relations_by_horizon.get(horizon.horizon_id, set()) + ), + mechanism_ids=tuple( + mechanisms_by_horizon.get(horizon.horizon_id, set()) + ), + ) + ) + + for mechanism in geometry.mechanisms: + if mechanism.post_verified: + continue + add( + _make_hole( + kind=SurfaceHoleKind.MECHANISM, + source_kind="MECHANISM", + source_id=mechanism.mechanism_id, + native_status="NOT_POST_VERIFIED", + description=( + f"Mechanism {mechanism.mechanism_id!r} is not post-verified." + ), + ordinary=False, + self_closure=True, + context=context, + mechanism_ids=(mechanism.mechanism_id,), + ) + ) + + for verification_order in geometry.verification_layers: + for coordinate_id in verification_order.coordinate_ids: + add_coordinate_hole( + coordinate_id, + ordinary=coordinate_id in surface_coordinate_ids, + layer_mechanisms=tuple(verification_order.mechanism_ids), + ) + if verification_order.order > 0 and not verification_order.evidence_ids: + add( + _make_hole( + kind=SurfaceHoleKind.VERIFICATION_ORDER, + source_kind="VERIFICATION_ORDER", + source_id=verification_order.layer_id, + native_status="MISSING_SUFFICIENCY_EVIDENCE", + description=( + f"Higher verification order {verification_order.layer_id!r} " + "has no sufficiency evidence." + ), + ordinary=False, + self_closure=True, + context=context, + mechanism_ids=tuple(verification_order.mechanism_ids), + ) + ) + + if geometry.secondary_subject_id is None: + add( + _make_hole( + kind=SurfaceHoleKind.SELF_CLOSURE_REQUIREMENT, + source_kind="GEOMETRY", + source_id=geometry.geometry_id, + native_status="MISSING_SECONDARY_SUBJECT", + description="Self-closure requires a secondary verification subject.", + ordinary=False, + self_closure=True, + context=context, + ) + ) + if not geometry.meta_focus_coordinate_ids: + add( + _make_hole( + kind=SurfaceHoleKind.SELF_CLOSURE_REQUIREMENT, + source_kind="GEOMETRY", + source_id=geometry.geometry_id, + native_status="MISSING_META_FOCUS", + description="Self-closure requires an explicit meta-focus.", + ordinary=False, + self_closure=True, + context=context, + ) + ) + orders = {item.order for item in geometry.verification_layers} + if not {0, 1}.issubset(orders): + add( + _make_hole( + kind=SurfaceHoleKind.SELF_CLOSURE_REQUIREMENT, + source_kind="GEOMETRY", + source_id=geometry.geometry_id, + native_status="MISSING_ADJACENT_V0_V1_ORDERS", + description="Self-closure requires adjacent V0 and V1 verification orders.", + ordinary=False, + self_closure=True, + context=context, + ) + ) + + return SurfaceAnalysis( + geometry_id=geometry.geometry_id, + geometry_digest=geometry.canonical_digest(), + context=context, + validity_errors=validity_errors, + ordinary_closed=assessment.ordinary_closed, + self_closed=assessment.self_closed, + ordinary_blockers=assessment.ordinary_blockers, + self_closure_blockers=assessment.self_closure_blockers, + holes=tuple(holes.values()), + ) + + +@dataclass(frozen=True) +class ValidationCandidate: + """One exact, nonexecuting association between a hole and a component.""" + + candidate_id: str + hole_id: str + status: CandidateStatus + component_id: Optional[str] + mechanism_id: Optional[str] + relation_id: Optional[str] + matching_basis: tuple[str, ...] + execution_prerequisites: tuple[str, ...] + blockers: tuple[str, ...] = () + plan_only: bool = field(default=True, init=False) + execution_performed: bool = field(default=False, init=False) + + def __post_init__(self) -> None: + _nonempty(self.candidate_id, "candidate_id") + _nonempty(self.hole_id, "hole_id") + try: + object.__setattr__(self, "status", CandidateStatus(self.status)) + except (TypeError, ValueError) as exc: + raise SurfaceAnalysisError(str(exc)) from exc + if self.component_id is not None: + _nonempty(self.component_id, "component_id") + if self.mechanism_id is not None: + _nonempty(self.mechanism_id, "mechanism_id") + if self.relation_id is not None: + _nonempty(self.relation_id, "relation_id") + for field_name in ("matching_basis", "execution_prerequisites", "blockers"): + object.__setattr__( + self, field_name, _unique_strings(getattr(self, field_name), field_name) + ) + if self.status is CandidateStatus.UNMATCHED: + if any( + value is not None + for value in (self.component_id, self.mechanism_id, self.relation_id) + ): + raise SurfaceAnalysisError( + "an unmatched candidate cannot identify a component or match coordinate" + ) + if not self.blockers: + raise SurfaceAnalysisError("an unmatched candidate must explain its blocker") + return + if self.component_id is None: + raise SurfaceAnalysisError("a matched candidate must identify a component") + if self.mechanism_id is None and self.relation_id is None: + raise SurfaceAnalysisError( + "a matched candidate must identify a relation or mechanism coordinate" + ) + if self.status is CandidateStatus.CANDIDATE and self.blockers: + raise SurfaceAnalysisError("an unblocked candidate cannot carry blockers") + if self.status is CandidateStatus.BLOCKED and not self.blockers: + raise SurfaceAnalysisError("a blocked candidate must explain its blockers") + + def to_dict(self) -> dict[str, Any]: + return { + "candidate_id": self.candidate_id, + "hole_id": self.hole_id, + "status": self.status.value, + "component_id": self.component_id, + "mechanism_id": self.mechanism_id, + "relation_id": self.relation_id, + "matching_basis": list(self.matching_basis), + "execution_prerequisites": list(self.execution_prerequisites), + "blockers": list(self.blockers), + "plan_only": self.plan_only, + "execution_performed": self.execution_performed, + } + + +@dataclass(frozen=True) +class ValidationPlan: + """Deterministic experimental plan; it has no execution path.""" + + plan_id: str + geometry_id: str + geometry_digest: str + registry_version: str + registry_digest: str + context: ControlSurfaceContext + candidates: tuple[ValidationCandidate, ...] + scope: str = MODELED_SURFACE_SCOPE + claim_boundary: str = PLAN_CLAIM_BOUNDARY + schema_version: str = PLAN_SCHEMA_VERSION + plan_only: bool = field(default=True, init=False) + execution_performed: bool = field(default=False, init=False) + + def __post_init__(self) -> None: + for field_name in ( + "plan_id", + "geometry_id", + "geometry_digest", + "registry_version", + "registry_digest", + ): + _nonempty(getattr(self, field_name), field_name) + if not isinstance(self.context, ControlSurfaceContext): + raise SurfaceAnalysisError("context must be a ControlSurfaceContext") + if self.scope != MODELED_SURFACE_SCOPE: + raise SurfaceAnalysisError(f"scope must be {MODELED_SURFACE_SCOPE!r}") + if self.schema_version != PLAN_SCHEMA_VERSION: + raise SurfaceAnalysisError(f"schema_version must be {PLAN_SCHEMA_VERSION!r}") + if self.claim_boundary != PLAN_CLAIM_BOUNDARY: + raise SurfaceAnalysisError( + "claim_boundary is fixed and cannot be weakened or replaced" + ) + if not isinstance(self.candidates, (tuple, list)): + raise SurfaceAnalysisError( + "candidates must be an array of ValidationCandidate records" + ) + candidates = tuple(self.candidates) + if not all(isinstance(item, ValidationCandidate) for item in candidates): + raise SurfaceAnalysisError("candidates must contain ValidationCandidate records") + candidate_ids = [item.candidate_id for item in candidates] + if len(set(candidate_ids)) != len(candidate_ids): + raise SurfaceAnalysisError("candidates must have unique identifiers") + object.__setattr__( + self, + "candidates", + tuple( + sorted( + candidates, + key=lambda item: ( + item.hole_id, + item.component_id or "", + item.mechanism_id or "", + item.relation_id or "", + item.status.value, + ), + ) + ), + ) + + @property + def unmatched_hole_ids(self) -> tuple[str, ...]: + return tuple( + item.hole_id + for item in self.candidates + if item.status is CandidateStatus.UNMATCHED + ) + + def to_dict(self) -> dict[str, Any]: + return { + "schema_version": self.schema_version, + "plan_id": self.plan_id, + "geometry_id": self.geometry_id, + "geometry_digest": self.geometry_digest, + "registry_version": self.registry_version, + "registry_digest": self.registry_digest, + "scope": self.scope, + "context": self.context.to_dict(), + "candidates": [candidate.to_dict() for candidate in self.candidates], + "plan_only": self.plan_only, + "execution_performed": self.execution_performed, + "claim_boundary": self.claim_boundary, + } + + def canonical_json_bytes(self) -> bytes: + return _canonical_json_bytes(self.to_dict()) + + +def _candidate_status( + component: InteroperabilityComponentDescriptor, +) -> tuple[CandidateStatus, tuple[str, ...]]: + blockers: list[str] = [] + if component.availability is not ComponentAvailability.AVAILABLE: + blockers.append(f"component availability is {component.availability.value}") + if component.lifecycle in { + ComponentLifecycle.UNSUPPORTED, + ComponentLifecycle.ABSENT, + }: + blockers.append(f"component lifecycle is {component.lifecycle.value}") + if blockers: + return CandidateStatus.BLOCKED, tuple(blockers) + return CandidateStatus.CANDIDATE, () + + +def _candidate( + *, + hole: SurfaceHole, + component: InteroperabilityComponentDescriptor, + mechanism_id: Optional[str], + relation_id: Optional[str], + context: ControlSurfaceContext, +) -> ValidationCandidate: + status, blockers = _candidate_status(component) + basis = [ + f"schema_id={hole.schema_id}", + f"interaction_mode={hole.interaction_mode.value}", + ] + if relation_id is not None: + basis.append(f"relation_id={relation_id}") + if mechanism_id is not None: + basis.append(f"mechanism_id={mechanism_id}") + identity = { + "hole_id": hole.hole_id, + "component_id": component.component_id, + "mechanism_id": mechanism_id, + "relation_id": relation_id, + "matching_basis": sorted(basis), + "status": status.value, + } + prerequisites = tuple( + dict.fromkeys( + ( + *EXECUTION_PREREQUISITES, + *component.execution_prerequisites, + *(f"TRUST_ROOT:{item}" for item in component.trust_roots), + *(f"DEPENDENCY:{item}" for item in component.optional_dependencies), + *(f"AUTHORITY:{item}" for item in context.authority_requirements), + ) + ) + ) + return ValidationCandidate( + candidate_id=_stable_identifier("validation-candidate", identity), + hole_id=hole.hole_id, + status=status, + component_id=component.component_id, + mechanism_id=mechanism_id, + relation_id=relation_id, + matching_basis=tuple(basis), + execution_prerequisites=prerequisites, + blockers=blockers, + ) + + +def _unmatched( + hole: SurfaceHole, context: ControlSurfaceContext +) -> ValidationCandidate: + sought = [ + f"schema_id={hole.schema_id}", + f"interaction_mode={hole.interaction_mode.value}", + ] + sought.extend(f"relation_id={item}" for item in hole.required_relations) + sought.extend(f"mechanism_id={item}" for item in hole.mechanism_ids) + identity = {"hole_id": hole.hole_id, "matching_basis": sorted(sought)} + return ValidationCandidate( + candidate_id=_stable_identifier("validation-candidate", identity), + hole_id=hole.hole_id, + status=CandidateStatus.UNMATCHED, + component_id=None, + mechanism_id=None, + relation_id=None, + matching_basis=tuple(sought), + execution_prerequisites=( + *EXECUTION_PREREQUISITES, + *(f"AUTHORITY:{item}" for item in context.authority_requirements), + ), + blockers=("no exact registered component match",), + ) + + +def plan_validation( + analysis: SurfaceAnalysis, + registry: InteroperabilityComponentRegistry, +) -> ValidationPlan: + """Match modeled holes to exact capabilities without selecting or executing one. + + If a hole names relations and mechanisms, a candidate is emitted only for an + exact pair matched by one descriptor. Separate partial matches cannot be + combined into a candidate. + """ + + if not isinstance(analysis, SurfaceAnalysis): + raise SurfaceAnalysisError("analysis must be a SurfaceAnalysis") + if not isinstance(registry, InteroperabilityComponentRegistry): + raise SurfaceAnalysisError( + "registry must be an InteroperabilityComponentRegistry" + ) + if analysis.validity_errors: + raise SurfaceAnalysisError( + "cannot plan validation for a structurally invalid verification geometry" + ) + + candidates: list[ValidationCandidate] = [] + for hole in analysis.holes: + matched: dict[tuple[str, Optional[str], Optional[str]], ValidationCandidate] = {} + if hole.required_relations and hole.mechanism_ids: + for relation_id in hole.required_relations: + for mechanism_id in hole.mechanism_ids: + for component in registry.match_exact( + schema_id=hole.schema_id, + interaction_mode=hole.interaction_mode, + relation_id=relation_id, + mechanism_id=mechanism_id, + ): + key = (component.component_id, mechanism_id, relation_id) + matched[key] = _candidate( + hole=hole, + component=component, + mechanism_id=mechanism_id, + relation_id=relation_id, + context=analysis.context, + ) + elif hole.required_relations: + for relation_id in hole.required_relations: + for component in registry.match_exact( + schema_id=hole.schema_id, + interaction_mode=hole.interaction_mode, + relation_id=relation_id, + ): + for mechanism_id in component.mechanism_ids: + key = (component.component_id, mechanism_id, relation_id) + matched[key] = _candidate( + hole=hole, + component=component, + mechanism_id=mechanism_id, + relation_id=relation_id, + context=analysis.context, + ) + elif hole.mechanism_ids: + for mechanism_id in hole.mechanism_ids: + for component in registry.match_exact( + schema_id=hole.schema_id, + interaction_mode=hole.interaction_mode, + mechanism_id=mechanism_id, + ): + key = (component.component_id, mechanism_id, None) + matched[key] = _candidate( + hole=hole, + component=component, + mechanism_id=mechanism_id, + relation_id=None, + context=analysis.context, + ) + if matched: + candidates.extend( + matched[key] + for key in sorted( + matched, + key=lambda item: (item[0], item[1] or "", item[2] or ""), + ) + ) + else: + candidates.append(_unmatched(hole, analysis.context)) + + registry_digest = registry.canonical_digest() + plan_identity = { + "geometry_id": analysis.geometry_id, + "geometry_digest": analysis.geometry_digest, + "registry_version": registry.registry_version, + "registry_digest": registry_digest, + "scope": analysis.scope, + "schema_id": analysis.context.schema_id, + "interaction_mode": analysis.context.interaction_mode.value, + "operating_regime": analysis.context.operating_regime, + "consequence_profiles": list(analysis.context.consequence_profiles), + "authority_requirements": list(analysis.context.authority_requirements), + "candidates": [ + candidate.to_dict() + for candidate in sorted(candidates, key=lambda item: item.candidate_id) + ], + } + return ValidationPlan( + plan_id=_stable_identifier("validation-plan", plan_identity), + geometry_id=analysis.geometry_id, + geometry_digest=analysis.geometry_digest, + registry_version=registry.registry_version, + registry_digest=registry_digest, + context=analysis.context, + candidates=tuple(candidates), + ) + + +generate_validation_plan = plan_validation + + +__all__ = [ + "ANALYSIS_CLAIM_BOUNDARY", + "ANALYSIS_INPUT_MODE", + "ANALYSIS_SCHEMA_VERSION", + "CandidateStatus", + "ControlSurfaceContext", + "EXECUTION_PREREQUISITES", + "MODELED_SURFACE_SCOPE", + "PLAN_CLAIM_BOUNDARY", + "PLAN_SCHEMA_VERSION", + "STRICT_WIRE_LOADING_STATUS", + "SurfaceAnalysis", + "SurfaceAnalysisError", + "SurfaceHole", + "SurfaceHoleKind", + "ValidationCandidate", + "ValidationPlan", + "analyze_verification_surface", + "generate_validation_plan", + "plan_validation", +] diff --git a/src/verifier/runtime/public_cli.py b/src/verifier/runtime/public_cli.py index 275fb9a..d5d71d8 100644 --- a/src/verifier/runtime/public_cli.py +++ b/src/verifier/runtime/public_cli.py @@ -26,6 +26,7 @@ verify_frozen_artifact, ) from verifier.core.checker import independence_is_evidenced +from verifier.core.platform_comparison import compare_platform_run_receipts from verifier.core.run import ( RunError, capture_run, @@ -235,6 +236,17 @@ def build_parser() -> argparse.ArgumentParser: plan_parser.add_argument("manifest", help="JSON or YAML run manifest.") plan_parser.add_argument("--json", action="store_true") + compare_platforms_parser = subparsers.add_parser( + "compare-platforms", + help="Compare declared generic-run result surfaces across operating systems.", + ) + compare_platforms_parser.add_argument( + "receipts", + nargs="+", + help="Receipt directories or receipt.json files, one per declared platform.", + ) + compare_platforms_parser.add_argument("--json", action="store_true") + for command, help_text in ( ("validate", "Run implemented receipt checks; Graph candidate validation is not conformance."), ("inspect", "Inspect a generic-run or VSTD-Graph receipt; validate and report VSTD-3."), @@ -605,6 +617,33 @@ def main(argv: list[str] | None = None) -> int: print(f" Canonical Digest: {receipt.canonical_digest}") return 0 if receipt.execution.outcome == "COMPLETED" else 1 + if args.command == "compare-platforms": + comparison = compare_platform_run_receipts(args.receipts) + report = comparison.to_dict() + if args.json: + print(json.dumps(report, indent=2, sort_keys=True)) + else: + print(f"[{comparison.status.value}] {comparison.reason}") + print( + " Required platforms: " + + (", ".join(comparison.required_platforms) or "none") + ) + print( + " Observed platforms: " + + (", ".join(comparison.observed_platforms) or "none") + ) + for error in comparison.errors: + print(f" Error: {error}") + for difference in comparison.differences: + print( + " Difference: " + f"{difference.get('reference_platform')} -> " + f"{difference.get('observed_platform')} at " + f"{difference.get('path')}" + ) + print(f" Boundary: {report['claim_boundary']}") + return comparison.exit_code + if args.command in {"validate", "inspect", "reproduce"}: return _handle_receipt_command(args) diff --git a/tests/test_control_surface.py b/tests/test_control_surface.py new file mode 100644 index 0000000..5f3cf27 --- /dev/null +++ b/tests/test_control_surface.py @@ -0,0 +1,626 @@ +"""Terminology: JavaScript Object Notation (JSON); Verifier Standard (VSTD). + +Adversarial tests for modeled-only surface analysis and nonexecuting plans. +""" + +import json +from dataclasses import replace + +import pytest + +from verifier.core.geometry import ( + Coordinate, + CoordinateJudgment, + CoordinateStatus, + Facet, + Grain, + Horizon, + HorizonKind, + Locus, + LocusKind, + Residual, + ResidualDisposition, + ResidualType, + Stratum, + Subject, + ValenceStatus, + VerificationGeometry, + VerificationLayer, + VerificationMechanism, + VerificationSurface, + VerificationValence, +) +from verifier.interoperability.catalog import ( + ComponentAvailability, + ComponentKind, + ComponentLifecycle, + InteractionMode, + InteroperabilityComponentDescriptor, + InteroperabilityComponentRegistry, +) +from verifier.interoperability.control_surface import ( + ANALYSIS_CLAIM_BOUNDARY, + ANALYSIS_INPUT_MODE, + MODELED_SURFACE_SCOPE, + PLAN_CLAIM_BOUNDARY, + STRICT_WIRE_LOADING_STATUS, + CandidateStatus, + ControlSurfaceContext, + SurfaceAnalysis, + SurfaceAnalysisError, + SurfaceHoleKind, + ValidationCandidate, + ValidationPlan, + analyze_verification_surface, + plan_validation, +) + + +def horizon_geometry( + coordinate_status: CoordinateStatus = CoordinateStatus.VERIFIED, +) -> VerificationGeometry: + return VerificationGeometry( + geometry_id="geometry:example", + primary_subject_id="subject:primary", + secondary_subject_id="subject:geometry", + subjects=[ + Subject("subject:primary", "Example process", "1"), + Subject( + "subject:geometry", + "Example verification geometry", + "1", + parent_subject_id="subject:primary", + ), + ], + loci=[ + Locus( + "locus:primary", + "subject:primary", + "example", + LocusKind.PROCESS, + Grain.SUBJECT, + Stratum.EXECUTION, + "example", + ), + Locus( + "locus:geometry", + "subject:geometry", + "geometry", + LocusKind.VERIFICATION_GEOMETRY, + Grain.SUBJECT, + Stratum.VERIFICATION, + "geometry:example", + ), + ], + facets=[ + Facet("facet:result", "result", "The bounded result."), + Facet("facet:sufficiency", "sufficiency", "The bounded geometry."), + ], + coordinates=[ + Coordinate("coordinate:result", "locus:primary", "facet:result"), + Coordinate( + "coordinate:sufficiency", "locus:geometry", "facet:sufficiency" + ), + ], + surface=VerificationSurface( + "surface:example", + "subject:primary", + ("coordinate:result",), + scope_statement="Only the declared example result.", + ), + mechanisms=[ + VerificationMechanism( + "mechanism:result", + "result verifier", + "1", + boundary_horizon_id="horizon:evidence", + ), + VerificationMechanism( + "mechanism:geometry", + "geometry verifier", + "1", + boundary_horizon_id="horizon:evidence", + ), + ], + judgments=[ + CoordinateJudgment( + "coordinate:result", + coordinate_status, + ("mechanism:result",), + ("evidence:result",), + ), + CoordinateJudgment( + "coordinate:sufficiency", + CoordinateStatus.INDETERMINATE, + ("mechanism:geometry",), + ("evidence:geometry-observation",), + ), + ], + horizons=[ + Horizon( + "horizon:evidence", + HorizonKind.EVIDENCE, + "No observation exists beyond the declared fixture.", + coordinate_id="coordinate:result", + ) + ], + residuals=[ + Residual( + "residual:observation", + ResidualType.BEHAVIORAL, + "The missing observation is explicitly bounded.", + observed="not captured", + represented_or_reconstructed="declared fixture only", + disposition=ResidualDisposition.HORIZON, + coordinate_id="coordinate:result", + horizon_id="horizon:evidence", + ) + ], + valences=[ + VerificationValence( + "valence:exact-evidence", + "COORDINATE", + "coordinate:result", + "HAS_EXACT_EVIDENCE", + "Exact evidence remains available only up to the horizon.", + status=ValenceStatus.HORIZON, + horizon_id="horizon:evidence", + ) + ], + verification_layers=[ + VerificationLayer( + "layer:v0", + 0, + "subject:primary", + coordinate_ids=("coordinate:result",), + mechanism_ids=("mechanism:result",), + evidence_ids=("evidence:result",), + ), + VerificationLayer( + "layer:v1", + 1, + "subject:geometry", + verifies_layer_id="layer:v0", + coordinate_ids=("coordinate:sufficiency",), + mechanism_ids=("mechanism:geometry",), + horizon_id="horizon:evidence", + ), + ], + focus_coordinate_ids=("coordinate:result",), + meta_focus_coordinate_ids=("coordinate:sufficiency",), + ) + + +def self_closed_geometry() -> VerificationGeometry: + geometry = horizon_geometry() + geometry.horizons.clear() + geometry.residuals[0] = replace( + geometry.residuals[0], + disposition=ResidualDisposition.RESOLVED, + horizon_id=None, + ) + geometry.valences[0] = replace( + geometry.valences[0], + status=ValenceStatus.DISCHARGED, + evidence_ids=("evidence:exact",), + horizon_id=None, + ) + geometry.mechanisms[0] = replace( + geometry.mechanisms[0], + post_verified=True, + post_verification_evidence_ids=("evidence:result-mechanism",), + boundary_horizon_id=None, + ) + geometry.mechanisms[1] = replace( + geometry.mechanisms[1], + post_verified=True, + post_verification_evidence_ids=("evidence:geometry-mechanism",), + boundary_horizon_id=None, + ) + geometry.judgments[1] = replace( + geometry.judgments[1], + status=CoordinateStatus.VERIFIED, + evidence_ids=("evidence:geometry",), + ) + geometry.verification_layers[1] = replace( + geometry.verification_layers[1], + evidence_ids=("evidence:geometry",), + horizon_id=None, + ) + return geometry + + +def component( + component_id: str, + *, + relation: str = "HAS_EXACT_EVIDENCE", + mechanism: str = "mechanism:candidate", + schema: str = "VSTD-2", + mode: InteractionMode = InteractionMode.STATIC, + tags: tuple[str, ...] = (), + optional_dependencies: tuple[str, ...] = (), + trust_roots: tuple[str, ...] = (), +) -> InteroperabilityComponentDescriptor: + return InteroperabilityComponentDescriptor( + component_id=component_id, + label=component_id, + kind=ComponentKind.VERIFIER, + lifecycle=ComponentLifecycle.IMPLEMENTED, + implementation_ref=f"example:{component_id}", + accepted_schema_ids=(schema,), + supported_relations=(relation,), + mechanism_ids=(mechanism,), + interaction_modes=(mode,), + domain_tags=tags, + optional_dependencies=optional_dependencies, + trust_roots=trust_roots, + availability=ComponentAvailability.AVAILABLE, + claim_boundary="Candidate association only.", + ) + + +@pytest.mark.parametrize( + "status", + [ + CoordinateStatus.INDETERMINATE, + CoordinateStatus.UNSUPPORTED, + CoordinateStatus.STALE, + ], +) +def test_native_unknown_like_coordinate_statuses_are_not_collapsed( + status: CoordinateStatus, +) -> None: + analysis = analyze_verification_surface(horizon_geometry(status)) + holes = [ + hole + for hole in analysis.holes + if hole.source_id == "coordinate:result" + and hole.kind is SurfaceHoleKind.COORDINATE_STATUS + ] + + assert [hole.native_status for hole in holes] == [status.value] + assert analysis.ordinary_closed is False + assert "UNKNOWN" not in {hole.native_status for hole in holes} + + +def test_horizon_allows_ordinary_closure_but_remains_a_self_closure_hole() -> None: + analysis = analyze_verification_surface(horizon_geometry()) + residual = next( + hole for hole in analysis.holes if hole.kind is SurfaceHoleKind.RESIDUAL + ) + horizon = next( + hole for hole in analysis.holes if hole.kind is SurfaceHoleKind.HORIZON + ) + + assert analysis.validity_errors == () + assert analysis.ordinary_closed is True + assert analysis.self_closed is False + assert residual.native_status == ResidualDisposition.HORIZON.value + assert residual.blocks_ordinary_closure is False + assert residual.blocks_self_closure is True + assert horizon.native_status == HorizonKind.EVIDENCE.value + assert horizon.blocks_ordinary_closure is False + assert horizon.blocks_self_closure is True + assert analysis.scope == MODELED_SURFACE_SCOPE + + +def test_self_closed_geometry_has_no_modeled_holes() -> None: + geometry = self_closed_geometry() + analysis = analyze_verification_surface(geometry) + + assert geometry.validate() == [] + assert geometry.assess_closure().self_closed is True + assert analysis.self_closed is True + assert analysis.holes == () + + +def test_analysis_accepts_only_typed_geometry_and_rejects_schema_substitution() -> None: + geometry = horizon_geometry() + + with pytest.raises(SurfaceAnalysisError, match="VerificationGeometry"): + analyze_verification_surface(geometry.to_dict()) # type: ignore[arg-type] + with pytest.raises(SurfaceAnalysisError, match="exactly equal"): + analyze_verification_surface( + geometry, + ControlSurfaceContext(schema_id="LOOKS-LIKE-VSTD-2"), + ) + + payload = analyze_verification_surface(geometry).to_dict() + assert payload["input_mode"] == ANALYSIS_INPUT_MODE + assert payload["strict_wire_loading_status"] == STRICT_WIRE_LOADING_STATUS + assert payload["scope"] == "MODELED_SURFACE_ONLY" + + +def test_malformed_public_inputs_raise_surface_analysis_errors() -> None: + analysis = analyze_verification_surface(horizon_geometry()) + registry = InteroperabilityComponentRegistry( + "1.3.0", (component("component:a"),) + ) + plan = plan_validation(analysis, registry) + + assert isinstance(analysis, SurfaceAnalysis) + assert isinstance(plan, ValidationPlan) + with pytest.raises(SurfaceAnalysisError, match="ControlSurfaceContext"): + analyze_verification_surface(horizon_geometry(), object()) # type: ignore[arg-type] + with pytest.raises(SurfaceAnalysisError, match="holes must be an array"): + replace(analysis, holes=None) # type: ignore[arg-type] + with pytest.raises(SurfaceAnalysisError, match="validity_errors must be an array"): + replace(analysis, validity_errors=None) # type: ignore[arg-type] + with pytest.raises(SurfaceAnalysisError, match="candidates must be an array"): + replace(plan, candidates=None) # type: ignore[arg-type] + + +def test_analysis_booleans_and_claim_boundaries_cannot_be_weakened() -> None: + analysis = analyze_verification_surface(horizon_geometry()) + plan = plan_validation( + analysis, + InteroperabilityComponentRegistry("1.3.0", (component("component:a"),)), + ) + + with pytest.raises(SurfaceAnalysisError, match="ordinary_closed must be a boolean"): + replace(analysis, ordinary_closed=1) + with pytest.raises(SurfaceAnalysisError, match="self_closed must be a boolean"): + replace(analysis, self_closed=0) + with pytest.raises(SurfaceAnalysisError, match="claim_boundary is fixed"): + replace(analysis, claim_boundary="Complete in the real world.") + with pytest.raises(SurfaceAnalysisError, match="claim_boundary is fixed"): + replace(plan, claim_boundary="Candidates establish assurance.") + assert analysis.claim_boundary == ANALYSIS_CLAIM_BOUNDARY + assert plan.claim_boundary == PLAN_CLAIM_BOUNDARY + + +def test_validation_candidate_states_have_coherent_coordinates_and_blockers() -> None: + baseline = { + "candidate_id": "candidate:example", + "hole_id": "hole:example", + "status": CandidateStatus.CANDIDATE, + "component_id": "component:example", + "mechanism_id": "mechanism:example", + "relation_id": None, + "matching_basis": ("schema_id=VSTD-2",), + "execution_prerequisites": ("BOUND_PROPOSITION",), + "blockers": (), + } + malformed_states = ( + { + "status": CandidateStatus.UNMATCHED, + "component_id": None, + "mechanism_id": "mechanism:example", + "blockers": ("no match",), + }, + { + "status": CandidateStatus.UNMATCHED, + "component_id": None, + "mechanism_id": None, + "blockers": (), + }, + {"mechanism_id": None, "relation_id": None}, + {"relation_id": "RELATION", "blockers": ("contradictory blocker",)}, + { + "status": CandidateStatus.BLOCKED, + "relation_id": "RELATION", + "blockers": (), + }, + ) + + for changes in malformed_states: + with pytest.raises(SurfaceAnalysisError): + ValidationCandidate(**{**baseline, **changes}) # type: ignore[arg-type] + + +def test_joint_relation_and_mechanism_matching_rejects_partial_matches() -> None: + analysis = analyze_verification_surface(horizon_geometry()) + horizon_hole = next( + hole for hole in analysis.holes if hole.kind is SurfaceHoleKind.HORIZON + ) + assert horizon_hole.required_relations == ("HAS_EXACT_EVIDENCE",) + assert "mechanism:result" in horizon_hole.mechanism_ids + registry = InteroperabilityComponentRegistry( + "1.3.0", + ( + component( + "component:full-match", + relation="HAS_EXACT_EVIDENCE", + mechanism="mechanism:result", + ), + component( + "component:relation-only", + relation="HAS_EXACT_EVIDENCE", + mechanism="mechanism:wrong", + ), + component( + "component:mechanism-only", + relation="WRONG_RELATION", + mechanism="mechanism:result", + ), + ), + ) + + candidates = [ + item + for item in plan_validation(analysis, registry).candidates + if item.hole_id == horizon_hole.hole_id + ] + + assert [ + (item.component_id, item.relation_id, item.mechanism_id) + for item in candidates + ] == [ + ("component:full-match", "HAS_EXACT_EVIDENCE", "mechanism:result") + ] + + +def test_candidate_prerequisites_name_component_dependencies_and_trust_roots() -> None: + analysis = analyze_verification_surface(horizon_geometry()) + valence_hole = next( + hole for hole in analysis.holes if hole.kind is SurfaceHoleKind.VALENCE + ) + registry = InteroperabilityComponentRegistry( + "1.3.0", + ( + component( + "component:bounded", + optional_dependencies=("package:optional",), + trust_roots=("trust-root:local",), + ), + ), + ) + candidate = next( + item + for item in plan_validation(analysis, registry).candidates + if item.hole_id == valence_hole.hole_id + and item.component_id == "component:bounded" + ) + + assert "DEPENDENCY:package:optional" in candidate.execution_prerequisites + assert "TRUST_ROOT:trust-root:local" in candidate.execution_prerequisites + + +def test_planning_rejects_analysis_of_structurally_invalid_geometry() -> None: + geometry = horizon_geometry() + geometry.coordinates.clear() + with pytest.raises(SurfaceAnalysisError, match="structurally invalid"): + analyze_verification_surface(geometry) + + analysis = replace( + analyze_verification_surface(horizon_geometry()), + validity_errors=("dangling coordinate reference",), + ) + + with pytest.raises(SurfaceAnalysisError, match="structurally invalid"): + plan_validation( + analysis, + InteroperabilityComponentRegistry( + "1.3.0", (component("component:a"),) + ), + ) + + +def test_exact_planning_retains_multiple_candidates_and_executes_nothing() -> None: + context = ControlSurfaceContext( + authority_requirements=("LOCAL_REVIEW",), + domain_tags=("software",), + ) + analysis = analyze_verification_surface(horizon_geometry(), context) + registry = InteroperabilityComponentRegistry( + "1.3.0", + ( + component("component:z", mechanism="mechanism:z"), + component("component:a", mechanism="mechanism:a"), + ), + ) + plan = plan_validation(analysis, registry) + valence_hole = next( + hole for hole in analysis.holes if hole.kind is SurfaceHoleKind.VALENCE + ) + candidates = [ + item for item in plan.candidates if item.hole_id == valence_hole.hole_id + ] + + assert [item.component_id for item in candidates] == [ + "component:a", + "component:z", + ] + assert all(item.status is CandidateStatus.CANDIDATE for item in candidates) + assert all(item.mechanism_id is not None for item in candidates) + assert all("AUTHORITY:LOCAL_REVIEW" in item.execution_prerequisites for item in candidates) + assert plan.plan_only is True + assert plan.execution_performed is False + assert all(item.plan_only is True for item in plan.candidates) + assert all(item.execution_performed is False for item in plan.candidates) + assert not hasattr(plan, "execute") + + +def test_zero_or_inexact_matches_remain_explicitly_unmatched() -> None: + analysis = analyze_verification_surface(horizon_geometry()) + valence_hole = next( + hole for hole in analysis.holes if hole.kind is SurfaceHoleKind.VALENCE + ) + registries = ( + InteroperabilityComponentRegistry("1.3.0", ()), + InteroperabilityComponentRegistry( + "1.3.0", (component("wrong-schema", schema="vstd-2"),) + ), + InteroperabilityComponentRegistry( + "1.3.0", (component("wrong-relation", relation="SIMILAR_RELATION"),) + ), + InteroperabilityComponentRegistry( + "1.3.0", + ( + component( + "wrong-mode", + mode=InteractionMode.OFFLINE_REPLAY, + ), + ), + ), + ) + + for registry in registries: + plan = plan_validation(analysis, registry) + candidates = [ + item for item in plan.candidates if item.hole_id == valence_hole.hole_id + ] + assert len(candidates) == 1 + assert candidates[0].status is CandidateStatus.UNMATCHED + assert candidates[0].component_id is None + + +def test_domain_tags_do_not_affect_matches_but_registry_bytes_bind_plan_id() -> None: + software_analysis = analyze_verification_surface( + horizon_geometry(), + ControlSurfaceContext(domain_tags=("software",)), + ) + biology_analysis = analyze_verification_surface( + horizon_geometry(), + ControlSurfaceContext(domain_tags=("biological",)), + ) + software_registry = InteroperabilityComponentRegistry( + "1.3.0", (component("component:a", tags=("software",)),) + ) + biology_registry = InteroperabilityComponentRegistry( + "1.3.0", (component("component:a", tags=("biological",)),) + ) + software_plan = plan_validation(software_analysis, software_registry) + biology_plan = plan_validation(biology_analysis, biology_registry) + + assert [hole.hole_id for hole in software_analysis.holes] == [ + hole.hole_id for hole in biology_analysis.holes + ] + assert [ + (item.candidate_id, item.hole_id, item.component_id, item.status) + for item in software_plan.candidates + ] == [ + (item.candidate_id, item.hole_id, item.component_id, item.status) + for item in biology_plan.candidates + ] + assert software_plan.registry_digest == software_registry.canonical_digest() + assert biology_plan.registry_digest == biology_registry.canonical_digest() + assert software_plan.registry_digest != biology_plan.registry_digest + assert software_plan.plan_id != biology_plan.plan_id + + +def test_analysis_and_plan_serialization_are_deterministic_and_have_no_pass() -> None: + first_analysis = analyze_verification_surface(horizon_geometry()) + second_analysis = analyze_verification_surface(horizon_geometry()) + first_registry = InteroperabilityComponentRegistry( + "1.3.0", + (component("component:z"), component("component:a")), + ) + second_registry = InteroperabilityComponentRegistry( + "1.3.0", + (component("component:a"), component("component:z")), + ) + first_plan = plan_validation(first_analysis, first_registry) + second_plan = plan_validation(second_analysis, second_registry) + + assert first_analysis.canonical_json_bytes() == second_analysis.canonical_json_bytes() + assert first_plan.canonical_json_bytes() == second_plan.canonical_json_bytes() + analysis_payload = json.loads(first_analysis.canonical_json_bytes()) + plan_payload = json.loads(first_plan.canonical_json_bytes()) + assert "expected_profile" not in analysis_payload + assert "aggregate_result" not in analysis_payload + assert "aggregate_result" not in plan_payload + assert plan_payload["registry_version"] == "1.3.0" + assert plan_payload["registry_digest"] == first_registry.canonical_digest() + assert plan_payload["plan_only"] is True + assert plan_payload["execution_performed"] is False diff --git a/tests/test_core_receipt_integrity.py b/tests/test_core_receipt_integrity.py index 362589a..262358d 100644 --- a/tests/test_core_receipt_integrity.py +++ b/tests/test_core_receipt_integrity.py @@ -8,7 +8,14 @@ from verifier.core.checker import IndependentAuditor from verifier.core.provenance import GitProvenance, ProvenanceRecord, RuntimeEnvironment -from verifier.core.receipt import ClaimSpec, EvidencePayload, VstdReceipt +from verifier.core.receipt import ( + ClaimSpec, + EvidencePayload, + StrictJsonError, + VstdReceipt, + canonical_json_dumps, + strict_json_loads, +) def _receipt() -> VstdReceipt: @@ -80,3 +87,27 @@ def test_recorded_digest_cannot_self_validate_after_claim_replacement() -> None: assert receipt.canonical_digest == original_digest assert not receipt.verify_digest_integrity() + + +@pytest.mark.parametrize( + "payload", + ( + '{"value": NaN}', + '{"value": Infinity}', + '{"value": -Infinity}', + '{"value": 1e999}', + '{"value": 1, "value": 2}', + '{"outer": {"value": 1, "value": 2}}', + ), +) +def test_strict_json_loader_rejects_non_finite_numbers_and_duplicate_keys( + payload: str, +) -> None: + with pytest.raises(StrictJsonError): + strict_json_loads(payload) + + +@pytest.mark.parametrize("value", (float("nan"), float("inf"), float("-inf"))) +def test_canonical_json_rejects_non_finite_numbers(value: float) -> None: + with pytest.raises(ValueError, match="Out of range float values"): + canonical_json_dumps({"value": value}) diff --git a/tests/test_generic_run.py b/tests/test_generic_run.py index 6a69f2e..b23997a 100644 --- a/tests/test_generic_run.py +++ b/tests/test_generic_run.py @@ -72,6 +72,29 @@ def test_digest_consistent_empty_generic_receipt_is_rejected(tmp_path, capsys): assert "missing required fields" in output +@pytest.mark.parametrize( + ("payload", "error_fragment"), + ( + ('{"value": NaN}', "non-finite number"), + ('{"value": 1e999}', "outside the finite float range"), + ('{"value": 1, "value": 2}', "duplicate object key"), + ), +) +def test_generic_validator_rejects_nonstandard_or_ambiguous_json( + tmp_path: Path, + capsys, + payload: str, + error_fragment: str, +) -> None: + path = tmp_path / "receipt.json" + path.write_text(payload, encoding="utf-8") + + assert validate_run_receipt(path) == 1 + output = capsys.readouterr().out + assert "[INTEGRITY OK]" not in output + assert error_fragment in output + + def _base_manifest() -> dict: return { "claim": { diff --git a/tests/test_graph_level.py b/tests/test_graph_level.py index fb7e06d..c41789d 100644 --- a/tests/test_graph_level.py +++ b/tests/test_graph_level.py @@ -24,6 +24,7 @@ ResourceBounds, Verdict, ) +from verifier.core.evidence import EvidenceStore, VerificationSession from verifier.core.kernel import KernelOutcome, check, is_horn, reference_descriptor from verifier.data.graph_level import ( GRAPH_MAX_LEVEL, @@ -34,6 +35,7 @@ ObligationKind, certify_graph_cnf, encode, + establish_graph_level, graph_level, obligations, ) @@ -206,10 +208,51 @@ def test_current_admissibility_changes_without_rewriting_historical_graph(curren assert historical.to_dict() == historical_bytes -def test_an_artifact_missing_from_the_graph_is_unknown_not_absent(): +def test_a_dangling_port_is_invalid_structure_not_a_candidate_result(): graph = _graph() del graph.artifacts["mid"] - assert _level(graph, _collection()).level == 0 + with pytest.raises(GraphEncodingError, match="references missing artifact mid"): + _level(graph, _collection()) + + +@pytest.mark.parametrize( + ("malformation", "message"), + ( + ("zero-input", "transformation t1 has no inputs"), + ("zero-output", "transformation t1 has no outputs"), + ("empty-role", "transformation t1 has an empty role for src"), + ("invalid-digest", "artifact src has an invalid content_digest"), + ), +) +@pytest.mark.parametrize("entrypoint", ("candidate", "evidence-bound")) +def test_structurally_invalid_graphs_are_refused_before_graph_establishment( + malformation, message, entrypoint +): + graph = _graph() + if malformation == "zero-input": + graph.transformations["t1"] = replace(graph.transformations["t1"], inputs=()) + elif malformation == "zero-output": + graph.transformations["t1"] = replace(graph.transformations["t1"], outputs=()) + elif malformation == "empty-role": + graph.transformations["t1"] = replace( + graph.transformations["t1"], inputs=(HyperedgePort("src", ""),) + ) + else: + graph.artifacts["src"] = replace(graph.artifacts["src"], content_digest="invalid") + + with pytest.raises(GraphEncodingError, match=message): + if entrypoint == "candidate": + _level(graph, _collection()) + else: + establish_graph_level( + graph, + collection_id="collection:C", + members=("corpus",), + object_evidence={}, + edge_evidence={}, + session=VerificationSession(EvidenceStore()), + binding=_binding(), + ) def test_cyclic_ancestry_cannot_receive_a_clean_candidate_level(): diff --git a/tests/test_interoperability_catalog.py b/tests/test_interoperability_catalog.py new file mode 100644 index 0000000..2ab3b58 --- /dev/null +++ b/tests/test_interoperability_catalog.py @@ -0,0 +1,234 @@ +"""Terminology: JavaScript Object Notation (JSON); Verifier Standard (VSTD). + +Adversarial tests for the domain-neutral VSTD interoperability catalog. +""" + +from dataclasses import FrozenInstanceError, replace + +import pytest + +from verifier.interoperability.catalog import ( + CatalogError, + ComponentAvailability, + ComponentKind, + ComponentLifecycle, + InteractionMode, + InteroperabilityComponentDescriptor, + InteroperabilityComponentRegistry, +) + + +def component( + component_id: str, + *, + schema_id: str = "VSTD-2", + relation_id: str = "HAS_EXACT_EVIDENCE", + mechanism_id: str = "mechanism:exact", + interaction_mode: InteractionMode = InteractionMode.STATIC, + domain_tags: tuple[str, ...] = (), +) -> InteroperabilityComponentDescriptor: + return InteroperabilityComponentDescriptor( + component_id=component_id, + label=component_id, + kind=ComponentKind.VERIFIER, + lifecycle=ComponentLifecycle.IMPLEMENTED, + implementation_ref=f"verifier.example:{component_id}", + accepted_schema_ids=(schema_id,), + verifier_family_ids=("schema-contract",), + native_system="example", + native_objects=("record",), + native_versions=("1",), + native_inputs=("application/json",), + native_outputs=("example-result",), + native_result_vocabulary=("PASS", "FAIL", "UNKNOWN"), + supported_relations=(relation_id,), + mechanism_ids=(mechanism_id,), + interaction_modes=(interaction_mode,), + domain_tags=domain_tags, + trust_roots=("local-fixture",), + freshness_behavior="Caller supplies the observation coordinate.", + transformation_loss="No transformation is performed by the descriptor.", + failure_behavior="Native non-passing states remain native.", + availability=ComponentAvailability.AVAILABLE, + claim_boundary="Catalog membership does not establish a native result.", + ) + + +def test_descriptor_and_registry_are_frozen() -> None: + descriptor = component("component:a") + registry = InteroperabilityComponentRegistry("1.3.0", (descriptor,)) + + with pytest.raises(FrozenInstanceError): + descriptor.label = "changed" # type: ignore[misc] + with pytest.raises(FrozenInstanceError): + registry.registry_version = "changed" # type: ignore[misc] + + +def test_matching_is_exact_on_schema_relation_mechanism_and_interaction_mode() -> None: + registry = InteroperabilityComponentRegistry("1.3.0", (component("component:a"),)) + + assert [ + item.component_id + for item in registry.match_exact( + schema_id="VSTD-2", + relation_id="HAS_EXACT_EVIDENCE", + mechanism_id="mechanism:exact", + interaction_mode=InteractionMode.STATIC, + ) + ] == ["component:a"] + assert ( + registry.match_exact( + schema_id="vstd-2", + relation_id="HAS_EXACT_EVIDENCE", + mechanism_id="mechanism:exact", + interaction_mode=InteractionMode.STATIC, + ) + == () + ) + assert ( + registry.match_exact( + schema_id="VSTD-2", + relation_id="has_exact_evidence", + mechanism_id="mechanism:exact", + interaction_mode=InteractionMode.STATIC, + ) + == () + ) + assert ( + registry.match_exact( + schema_id="VSTD-2", + relation_id="HAS_EXACT_EVIDENCE", + mechanism_id="mechanism:other", + interaction_mode=InteractionMode.STATIC, + ) + == () + ) + assert ( + registry.match_exact( + schema_id="VSTD-2", + relation_id="HAS_EXACT_EVIDENCE", + mechanism_id="mechanism:exact", + interaction_mode=InteractionMode.OFFLINE_REPLAY, + ) + == () + ) + + +def test_descriptor_relations_and_mechanisms_form_an_explicit_cartesian_product() -> None: + descriptor = replace( + component("component:a"), + supported_relations=("RELATION:A", "RELATION:B"), + mechanism_ids=("mechanism:a", "mechanism:b"), + ) + + for relation_id in descriptor.supported_relations: + for mechanism_id in descriptor.mechanism_ids: + assert descriptor.matches_exact( + schema_id="VSTD-2", + interaction_mode=InteractionMode.STATIC, + relation_id=relation_id, + mechanism_id=mechanism_id, + ) + + +def test_domain_tags_are_metadata_and_do_not_change_match_results() -> None: + software = InteroperabilityComponentRegistry( + "1.3.0", (component("component:a", domain_tags=("software",)),) + ) + biological = InteroperabilityComponentRegistry( + "1.3.0", (component("component:a", domain_tags=("biological",)),) + ) + query = { + "schema_id": "VSTD-2", + "relation_id": "HAS_EXACT_EVIDENCE", + "interaction_mode": InteractionMode.STATIC, + } + + assert [item.component_id for item in software.match_exact(**query)] == [ + item.component_id for item in biological.match_exact(**query) + ] + assert software.components[0].domain_tags != biological.components[0].domain_tags + + +def test_registry_retains_multiple_matches_and_returns_zero_without_substitution() -> None: + registry = InteroperabilityComponentRegistry( + "1.3.0", + ( + component("component:z"), + component("component:a"), + component("component:other", relation_id="OTHER_RELATION"), + ), + ) + + matches = registry.match_exact( + schema_id="VSTD-2", + relation_id="HAS_EXACT_EVIDENCE", + interaction_mode=InteractionMode.STATIC, + ) + none = registry.match_exact( + schema_id="VSTD-2", + relation_id="SIMILAR_LOOKING_RELATION", + interaction_mode=InteractionMode.STATIC, + ) + + assert [item.component_id for item in matches] == ["component:a", "component:z"] + assert none == () + + +def test_registry_serialization_is_deterministic_and_strictly_round_trips() -> None: + first = InteroperabilityComponentRegistry( + "1.3.0", + ( + component("component:z", domain_tags=("zeta", "alpha")), + component("component:a"), + ), + ) + second = InteroperabilityComponentRegistry( + "1.3.0", + ( + component("component:a"), + component("component:z", domain_tags=("alpha", "zeta")), + ), + ) + + assert first.canonical_json_bytes() == second.canonical_json_bytes() + assert first.canonical_digest() == second.canonical_digest() + assert ( + InteroperabilityComponentRegistry.from_dict(first.to_dict()).canonical_json_bytes() + == first.canonical_json_bytes() + ) + + malformed = first.to_dict() + malformed["unexpected"] = True + with pytest.raises(CatalogError, match="extra=.*unexpected"): + InteroperabilityComponentRegistry.from_dict(malformed) + + +def test_public_loaders_reject_non_objects_with_catalog_errors() -> None: + for malformed in (None, [], "not-an-object"): + with pytest.raises(CatalogError, match="descriptor must be an object"): + InteroperabilityComponentDescriptor.from_dict(malformed) # type: ignore[arg-type] + with pytest.raises(CatalogError, match="registry must be an object"): + InteroperabilityComponentRegistry.from_dict(malformed) # type: ignore[arg-type] + + +def test_semantic_identifiers_reject_surrounding_whitespace() -> None: + baseline = component("component:a") + malformed_descriptors = ( + {"component_id": " component:a"}, + {"label": "component a "}, + {"implementation_ref": " verifier.example:component:a"}, + {"accepted_schema_ids": ("VSTD-2 ",)}, + {"supported_relations": (" HAS_EXACT_EVIDENCE",)}, + {"mechanism_ids": ("mechanism:exact ",)}, + {"domain_tags": (" software",)}, + ) + + for changes in malformed_descriptors: + with pytest.raises(CatalogError, match="surrounding whitespace"): + replace(baseline, **changes) + + with pytest.raises(CatalogError, match="surrounding whitespace"): + InteroperabilityComponentRegistry(" 1.3.0", (baseline,)) + with pytest.raises(CatalogError, match="surrounding whitespace"): + InteroperabilityComponentRegistry("1.3.0", (baseline,)).get("component:a ") diff --git a/tests/test_interoperability_planning_example.py b/tests/test_interoperability_planning_example.py new file mode 100644 index 0000000..785c686 --- /dev/null +++ b/tests/test_interoperability_planning_example.py @@ -0,0 +1,155 @@ +"""Terminology: identifier (ID). + +Characterization tests for the experimental interoperability planning example. +""" + +from __future__ import annotations + +import importlib.util +import json +import os +from pathlib import Path +import subprocess +import sys + +import verifier +import verifier.interoperability as interoperability + + +ROOT = Path(__file__).resolve().parents[1] +DEMO = ROOT / "examples" / "interoperability_planning" / "demo.py" +EXPECTED_FACADE = [ + "CandidateStatus", + "CatalogError", + "ComponentAvailability", + "ComponentKind", + "ComponentLifecycle", + "ControlSurfaceContext", + "InteractionMode", + "InteroperabilityComponentDescriptor", + "InteroperabilityComponentRegistry", + "SurfaceAnalysis", + "SurfaceAnalysisError", + "SurfaceHole", + "SurfaceHoleKind", + "ValidationCandidate", + "ValidationPlan", + "analyze_verification_surface", + "plan_validation", +] + + +def _load_demo(): + spec = importlib.util.spec_from_file_location("vstd_interoperability_demo", DEMO) + assert spec is not None and spec.loader is not None + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +def test_experimental_facade_is_exact_and_not_promoted_to_top_level() -> None: + assert interoperability.__all__ == EXPECTED_FACADE + for name in EXPECTED_FACADE: + assert getattr(interoperability, name) is not None + assert name not in verifier.__all__ + assert "InteroperabilityCatalog" not in interoperability.__all__ + assert "generate_validation_plan" not in interoperability.__all__ + + +def test_example_detects_two_candidates_without_running_checker() -> None: + demo = _load_demo() + geometry = demo.build_geometry() + registry = demo.build_registry() + assert geometry.validate() == [] + assert demo._checker_invocations == 0 + + first = demo.build_demo() + second = demo.build_demo() + + assert first == second + assert first["summary"] == { + "checker_invocations": 0, + "exact_candidate_count": 2, + "execution_performed": False, + "hole_count": 5, + "ordinary_closed": False, + "plan_only": True, + "self_closed": False, + "unmatched_hole_count": 3, + } + assert demo._checker_invocations == 0 + assert first["plan"]["registry_version"] == registry.registry_version + assert first["plan"]["registry_digest"] == registry.canonical_digest() + assert first["plan"]["plan_only"] is True + assert first["plan"]["execution_performed"] is False + + candidates = first["plan"]["candidates"] + matched = [item for item in candidates if item["status"] == "CANDIDATE"] + unmatched = [item for item in candidates if item["status"] == "UNMATCHED"] + holes_by_id = { + item["hole_id"]: item for item in first["analysis"]["holes"] + } + matched_holes = {holes_by_id[item["hole_id"]]["kind"] for item in matched} + assert matched_holes == {"COORDINATE_STATUS", "MECHANISM"} + assert { + ( + holes_by_id[item["hole_id"]]["kind"], + holes_by_id[item["hole_id"]]["native_status"], + ) + for item in unmatched + } == { + ("SELF_CLOSURE_REQUIREMENT", "MISSING_ADJACENT_V0_V1_ORDERS"), + ("SELF_CLOSURE_REQUIREMENT", "MISSING_META_FOCUS"), + ("SELF_CLOSURE_REQUIREMENT", "MISSING_SECONDARY_SUBJECT"), + } + assert {item["mechanism_id"] for item in matched} == { + demo.CHECKER_MECHANISM_ID + } + assert {item["relation_id"] for item in matched} == {None} + assert {item["component_id"] for item in matched} == { + "component:lexicographic-check" + } + assert all(item["component_id"] is None for item in unmatched) + + +def test_example_rendering_is_deterministic_and_claim_bounded() -> None: + demo = _load_demo() + first = demo.render_demo() + second = demo.render_demo() + assert first == second + parsed = json.loads(first) + assert parsed["summary"]["checker_invocations"] == 0 + assert parsed["analysis"]["strict_wire_loading_status"] == "UNSUPPORTED" + assert "do not establish" in parsed["plan"]["claim_boundary"] + + +def test_documented_example_command_is_deterministic_from_checkout() -> None: + environment = dict(os.environ) + source_path = str(ROOT / "src") + existing = environment.get("PYTHONPATH") + environment["PYTHONPATH"] = ( + source_path if not existing else os.pathsep.join((source_path, existing)) + ) + command = [sys.executable, str(DEMO)] + first = subprocess.run( + command, + cwd=ROOT, + env=environment, + check=True, + capture_output=True, + text=True, + ).stdout + second = subprocess.run( + command, + cwd=ROOT, + env=environment, + check=True, + capture_output=True, + text=True, + ).stdout + + assert first == second + parsed = json.loads(first) + assert parsed["summary"]["checker_invocations"] == 0 + assert parsed["summary"]["exact_candidate_count"] == 2 + assert parsed["summary"]["unmatched_hole_count"] == 3 diff --git a/tests/test_platform_comparison.py b/tests/test_platform_comparison.py new file mode 100644 index 0000000..9f15b3c --- /dev/null +++ b/tests/test_platform_comparison.py @@ -0,0 +1,636 @@ +"""Terminology: command-line interface (CLI); JavaScript Object Notation (JSON); +Verifier Standard (VSTD). + +Adversarial tests for bounded operating-system result comparison. +""" + +from __future__ import annotations + +import copy +import json +from pathlib import Path +import subprocess +import sys +from typing import Any, Callable + +import pytest + +from verifier.core.platform_comparison import ( + PlatformComparisonStatus, + compare_platform_run_receipts, +) +from verifier.core.receipt import compute_canonical_digest +from verifier.core.run import capture_run +from verifier.core.run_validation import _rebuild_stable_payload_from_dict +from verifier.runtime.public_cli import main + + +PLATFORMS = ("Darwin", "Linux", "Windows") +ROOT = Path(__file__).resolve().parents[1] + + +def _captured_receipt(project: Path) -> dict[str, Any]: + project.mkdir() + (project / "data").mkdir() + (project / "src").mkdir() + (project / "data" / "input.txt").write_text( + "portable input\n", encoding="utf-8" + ) + (project / "src" / "compute.py").write_text( + "from pathlib import Path\n" + "Path('output.txt').write_bytes(Path('data/input.txt').read_bytes().upper())\n", + encoding="utf-8", + ) + manifest = { + "claim": { + "id": "RUN-PLATFORM-TEST", + "title": "Operating-system comparison fixture", + "statement": "The declared command records the same output bytes.", + "scope": "one test fixture", + "limitations": ["supplied receipt coordinates only"], + "falsification_condition": "a comparable declared result differs", + }, + "command": [sys.executable, "src/compute.py"], + "cwd": ".", + "repo_dir": ".", + "target_name": "platform-comparison-fixture", + "portable_repository_id": "example.invalid/verifier-fixture", + "inputs": [ + {"path": "data/input.txt", "role": "primary_input"}, + {"path": "src/compute.py", "role": "entrypoint_source"}, + ], + "outputs": [{"path": "output.txt", "role": "primary_output"}], + "determinism_declared": "DETERMINISTIC", + "evaluator_claims": [ + { + "evaluator_name": "fixture_byte_counter", + "metric_name": "output_bytes", + "value": 15, + } + ], + "refutation_surface": { + "admissible_refutations": ["a declared result differs"], + "excluded_claims": ["UNIVERSAL_PORTABILITY"], + "falsification_condition": "a comparable declared result differs", + "platform_comparability": { + "mechanism_id": "PLATFORM-COMPARISON-TEST-SUBJECT-1", + "compatible_platforms": list(PLATFORMS), + "result_surfaces": [ + "execution", + "declared_outputs", + "evaluator_claims", + "stdio", + ], + }, + }, + } + return capture_run(manifest, manifest_dir=project).to_dict() + + +def _write_variant( + root: Path, + base: dict[str, Any], + platform: str, + *, + mutate: Callable[[dict[str, Any]], None] | None = None, + execution_platform: str | None = None, +) -> Path: + receipt = copy.deepcopy(base) + receipt["source_state"]["runtime"]["platform_system"] = platform + receipt["execution"]["platform_system"] = execution_platform or platform + if mutate is not None: + mutate(receipt) + receipt["canonical_digest"] = compute_canonical_digest( + _rebuild_stable_payload_from_dict(receipt) + ) + root.mkdir() + (root / "receipt.json").write_text( + json.dumps(receipt, indent=2, sort_keys=True), encoding="utf-8" + ) + return root + + +def _three_receipts(tmp_path: Path) -> tuple[dict[str, Any], list[Path]]: + base = _captured_receipt(tmp_path / "project") + receipts = [ + _write_variant(tmp_path / platform.lower(), base, platform) + for platform in PLATFORMS + ] + return base, receipts + + +def test_identical_declared_results_across_all_platforms_pass(tmp_path: Path) -> None: + _base, receipts = _three_receipts(tmp_path) + + result = compare_platform_run_receipts(receipts) + + assert result.status is PlatformComparisonStatus.PASS + assert result.exit_code == 0 + assert result.required_platforms == PLATFORMS + assert result.observed_platforms == PLATFORMS + assert result.comparison_binding_digest is not None + assert {item["platform"] for item in result.observations} == set(PLATFORMS) + assert len({item["result_digest"] for item in result.observations}) == 1 + + +def test_capture_uses_platform_neutral_nested_source_paths(tmp_path: Path) -> None: + receipt = _captured_receipt(tmp_path / "project") + + assert set(receipt["source_state"]["source_file_hashes"]) == { + "data/input.txt", + "src/compute.py", + } + + +def test_missing_declared_platform_is_not_established(tmp_path: Path) -> None: + _base, receipts = _three_receipts(tmp_path) + + result = compare_platform_run_receipts(receipts[:-1]) + + assert result.status is PlatformComparisonStatus.NOT_ESTABLISHED + assert result.exit_code == 2 + assert result.errors == ("missing declared platforms: Windows",) + + +def test_comparable_output_disagreement_is_conflicted(tmp_path: Path) -> None: + base = _captured_receipt(tmp_path / "project") + receipts = [ + _write_variant(tmp_path / "darwin", base, "Darwin"), + _write_variant(tmp_path / "linux", base, "Linux"), + _write_variant( + tmp_path / "windows", + base, + "Windows", + mutate=lambda receipt: receipt["outputs"][0].__setitem__( + "sha256", "0" * 64 + ), + ), + ] + + result = compare_platform_run_receipts(receipts) + + assert result.status is PlatformComparisonStatus.CONFLICTED + assert result.exit_code == 1 + assert result.comparison_binding_digest is not None + assert any( + difference["path"] == "declared_outputs[0].sha256" + and difference["observed_platform"] == "Windows" + for difference in result.differences + ) + + +def test_comparable_evaluator_value_disagreement_is_conflicted( + tmp_path: Path, +) -> None: + base = _captured_receipt(tmp_path / "project") + receipts = [ + _write_variant(tmp_path / "darwin", base, "Darwin"), + _write_variant(tmp_path / "linux", base, "Linux"), + _write_variant( + tmp_path / "windows", + base, + "Windows", + mutate=lambda receipt: receipt["claims"]["evaluator_claims"][ + 0 + ].__setitem__("value", 16), + ), + ] + + result = compare_platform_run_receipts(receipts) + + assert result.status is PlatformComparisonStatus.CONFLICTED + assert any( + difference["path"] == "evaluator_claims[0].value" + for difference in result.differences + ) + + +@pytest.mark.parametrize( + ("reference_value", "observed_value"), + ((15, 15.0), (0.0, -0.0), (True, 1)), +) +def test_canonical_json_value_representation_drift_is_conflicted( + tmp_path: Path, + reference_value: object, + observed_value: object, +) -> None: + base = _captured_receipt(tmp_path / "project") + base["claims"]["evaluator_claims"][0]["value"] = reference_value + receipts = [ + _write_variant(tmp_path / "darwin", base, "Darwin"), + _write_variant(tmp_path / "linux", base, "Linux"), + _write_variant( + tmp_path / "windows", + base, + "Windows", + mutate=lambda receipt: receipt["claims"]["evaluator_claims"][ + 0 + ].__setitem__("value", observed_value), + ), + ] + + result = compare_platform_run_receipts(receipts) + + assert result.status is PlatformComparisonStatus.CONFLICTED + assert any( + difference["path"] == "evaluator_claims[0].value" + for difference in result.differences + ) + + +def test_output_identity_drift_is_not_established(tmp_path: Path) -> None: + base = _captured_receipt(tmp_path / "project") + receipts = [ + _write_variant(tmp_path / "darwin", base, "Darwin"), + _write_variant(tmp_path / "linux", base, "Linux"), + _write_variant( + tmp_path / "windows", + base, + "Windows", + mutate=lambda receipt: receipt["outputs"][0].__setitem__( + "path", "different-output.txt" + ), + ), + ] + + result = compare_platform_run_receipts(receipts) + + assert result.status is PlatformComparisonStatus.NOT_ESTABLISHED + assert any( + difference["kind"] == "comparison_binding" + and difference["path"] == "outputs[0].path" + for difference in result.differences + ) + + +def test_evaluator_identity_drift_is_not_established(tmp_path: Path) -> None: + base = _captured_receipt(tmp_path / "project") + receipts = [ + _write_variant(tmp_path / "darwin", base, "Darwin"), + _write_variant(tmp_path / "linux", base, "Linux"), + _write_variant( + tmp_path / "windows", + base, + "Windows", + mutate=lambda receipt: receipt["claims"]["evaluator_claims"][ + 0 + ].__setitem__("metric_name", "different_metric"), + ), + ] + + result = compare_platform_run_receipts(receipts) + + assert result.status is PlatformComparisonStatus.NOT_ESTABLISHED + assert any( + difference["kind"] == "comparison_binding" + and difference["path"] + == "claims.evaluator_claims[0].metric_name" + for difference in result.differences + ) + + +def test_non_platform_binding_drift_is_not_mislabeled_conflict( + tmp_path: Path, +) -> None: + base = _captured_receipt(tmp_path / "project") + receipts = [ + _write_variant(tmp_path / "darwin", base, "Darwin"), + _write_variant( + tmp_path / "linux", + base, + "Linux", + mutate=lambda receipt: receipt.__setitem__( + "claim_scope", "different claim coordinate" + ), + ), + _write_variant(tmp_path / "windows", base, "Windows"), + ] + + result = compare_platform_run_receipts(receipts) + + assert result.status is PlatformComparisonStatus.NOT_ESTABLISHED + assert result.comparison_binding_digest is None + assert any( + difference["kind"] == "comparison_binding" + and difference["path"] == "claim_scope" + for difference in result.differences + ) + + +def test_machine_family_drift_is_not_mislabeled_operating_system_conflict( + tmp_path: Path, +) -> None: + base = _captured_receipt(tmp_path / "project") + + def change_machine_family(receipt: dict[str, Any]) -> None: + receipt["source_state"]["runtime"]["platform_machine"] = "arm64" + + receipts = [ + _write_variant(tmp_path / "darwin", base, "Darwin"), + _write_variant(tmp_path / "linux", base, "Linux"), + _write_variant( + tmp_path / "windows", + base, + "Windows", + mutate=change_machine_family, + ), + ] + + result = compare_platform_run_receipts(receipts) + + assert result.status is PlatformComparisonStatus.NOT_ESTABLISHED + assert any( + difference["path"] == "platform_comparison_environment.machine_family" + for difference in result.differences + ) + + +def test_missing_machine_coordinate_is_not_established(tmp_path: Path) -> None: + base = _captured_receipt(tmp_path / "project") + + def remove_machine(receipt: dict[str, Any]) -> None: + del receipt["source_state"]["runtime"]["platform_machine"] + + receipts = [ + _write_variant(tmp_path / "darwin", base, "Darwin"), + _write_variant(tmp_path / "linux", base, "Linux", mutate=remove_machine), + _write_variant(tmp_path / "windows", base, "Windows"), + ] + + result = compare_platform_run_receipts(receipts) + + assert result.status is PlatformComparisonStatus.NOT_ESTABLISHED + assert any("comparison environment is missing" in error for error in result.errors) + + +def test_canonical_digest_tampering_is_invalid(tmp_path: Path) -> None: + _base, receipts = _three_receipts(tmp_path) + receipt_path = receipts[1] / "receipt.json" + receipt = json.loads(receipt_path.read_text(encoding="utf-8")) + receipt["claim_scope"] = "tampered without digest update" + receipt_path.write_text(json.dumps(receipt), encoding="utf-8") + + result = compare_platform_run_receipts(receipts) + + assert result.status is PlatformComparisonStatus.INVALID + assert any("canonical digest mismatch" in error for error in result.errors) + + +@pytest.mark.parametrize( + ("payload", "error_fragment"), + ( + ('{"value": NaN}', "non-finite number"), + ('{"value": Infinity}', "non-finite number"), + ('{"value": -Infinity}', "non-finite number"), + ('{"value": 1e999}', "outside the finite float range"), + ('{"value": 1, "value": 2}', "duplicate object key"), + ), +) +def test_comparator_rejects_nonstandard_or_ambiguous_json( + tmp_path: Path, + payload: str, + error_fragment: str, +) -> None: + receipt_path = tmp_path / "receipt.json" + receipt_path.write_text(payload, encoding="utf-8") + + result = compare_platform_run_receipts([receipt_path]) + + assert result.status is PlatformComparisonStatus.INVALID + assert any(error_fragment in error for error in result.errors) + + +def test_absent_declaration_is_not_established(tmp_path: Path) -> None: + base = _captured_receipt(tmp_path / "project") + + def remove_declaration(receipt: dict[str, Any]) -> None: + del receipt["assessment_context"]["refutation_surface"][ + "platform_comparability" + ] + + receipts = [ + _write_variant( + tmp_path / platform.lower(), + base, + platform, + mutate=remove_declaration, + ) + for platform in PLATFORMS + ] + + result = compare_platform_run_receipts(receipts) + + assert result.status is PlatformComparisonStatus.NOT_ESTABLISHED + assert result.exit_code == 2 + assert len(result.errors) == 3 + + +def test_malformed_declaration_is_invalid(tmp_path: Path) -> None: + base = _captured_receipt(tmp_path / "project") + + def corrupt_mechanism(receipt: dict[str, Any]) -> None: + receipt["assessment_context"]["refutation_surface"][ + "platform_comparability" + ]["mechanism_id"] = "" + + receipts = [ + _write_variant(tmp_path / "darwin", base, "Darwin"), + _write_variant(tmp_path / "linux", base, "Linux", mutate=corrupt_mechanism), + _write_variant(tmp_path / "windows", base, "Windows"), + ] + + result = compare_platform_run_receipts(receipts) + + assert result.status is PlatformComparisonStatus.INVALID + assert result.exit_code == 1 + assert any("mechanism_id must identify" in error for error in result.errors) + + +def test_platform_identifiers_that_collide_after_normalization_are_invalid( + tmp_path: Path, +) -> None: + base = _captured_receipt(tmp_path / "project") + base["assessment_context"]["refutation_surface"]["platform_comparability"][ + "compatible_platforms" + ] = ["Linux", "Windows", "WINDOWS"] + receipts = [ + _write_variant(tmp_path / platform.lower(), base, platform) + for platform in PLATFORMS + ] + + result = compare_platform_run_receipts(receipts) + + assert result.status is PlatformComparisonStatus.INVALID + assert any( + "collide after Unicode normalization and casefold" in error + for error in result.errors + ) + + +def test_duplicate_platform_evidence_is_not_established(tmp_path: Path) -> None: + base = _captured_receipt(tmp_path / "project") + receipts = [ + _write_variant(tmp_path / "darwin", base, "Darwin"), + _write_variant(tmp_path / "linux-a", base, "Linux"), + _write_variant(tmp_path / "linux-b", base, "Linux"), + ] + + result = compare_platform_run_receipts(receipts) + + assert result.status is PlatformComparisonStatus.NOT_ESTABLISHED + assert any("duplicate receipt" in error for error in result.errors) + assert any("missing declared platforms: Windows" == error for error in result.errors) + + +def test_contradictory_platform_fields_are_invalid(tmp_path: Path) -> None: + base = _captured_receipt(tmp_path / "project") + receipts = [ + _write_variant(tmp_path / "darwin", base, "Darwin"), + _write_variant( + tmp_path / "linux", + base, + "Linux", + execution_platform="Windows", + ), + _write_variant(tmp_path / "windows", base, "Windows"), + ] + + result = compare_platform_run_receipts(receipts) + + assert result.status is PlatformComparisonStatus.INVALID + assert any("does not equal" in error for error in result.errors) + + +def test_declaration_order_is_semantically_normalized(tmp_path: Path) -> None: + base = _captured_receipt(tmp_path / "project") + + def reverse_declaration(receipt: dict[str, Any]) -> None: + declaration = receipt["assessment_context"]["refutation_surface"][ + "platform_comparability" + ] + declaration["compatible_platforms"].reverse() + declaration["result_surfaces"].reverse() + + receipts = [ + _write_variant(tmp_path / "darwin", base, "Darwin"), + _write_variant(tmp_path / "linux", base, "Linux", mutate=reverse_declaration), + _write_variant(tmp_path / "windows", base, "Windows"), + ] + + result = compare_platform_run_receipts(receipts) + + assert result.status is PlatformComparisonStatus.PASS + + +def test_legacy_windows_source_separators_compare_with_posix_paths( + tmp_path: Path, +) -> None: + base = _captured_receipt(tmp_path / "project") + + def use_legacy_windows_separators(receipt: dict[str, Any]) -> None: + source_hashes = receipt["source_state"]["source_file_hashes"] + receipt["source_state"]["source_file_hashes"] = { + path.replace("/", "\\"): digest + for path, digest in source_hashes.items() + } + + receipts = [ + _write_variant(tmp_path / "darwin", base, "Darwin"), + _write_variant(tmp_path / "linux", base, "Linux"), + _write_variant( + tmp_path / "windows", + base, + "Windows", + mutate=use_legacy_windows_separators, + ), + ] + + result = compare_platform_run_receipts(receipts) + + assert result.status is PlatformComparisonStatus.PASS + + +def test_colliding_legacy_windows_source_paths_are_invalid(tmp_path: Path) -> None: + base = _captured_receipt(tmp_path / "project") + + def add_separator_collision(receipt: dict[str, Any]) -> None: + source_hashes = receipt["source_state"]["source_file_hashes"] + source_hashes["src\\compute.py"] = source_hashes["src/compute.py"] + + receipts = [ + _write_variant(tmp_path / "darwin", base, "Darwin"), + _write_variant(tmp_path / "linux", base, "Linux"), + _write_variant( + tmp_path / "windows", + base, + "Windows", + mutate=add_separator_collision, + ), + ] + + result = compare_platform_run_receipts(receipts) + + assert result.status is PlatformComparisonStatus.INVALID + assert any( + "collide after Windows separator normalization" in error + for error in result.errors + ) + + +def test_compare_platforms_cli_emits_bounded_json_report( + tmp_path: Path, capsys +) -> None: + _base, receipts = _three_receipts(tmp_path) + + assert main( + ["compare-platforms", *(str(path) for path in receipts), "--json"] + ) == 0 + report = json.loads(capsys.readouterr().out) + + assert report["status"] == "PASS" + assert report["report_kind"] == "platform_comparison_diagnostic" + assert report["mechanism"]["identifier"] == "VSTD-PLATFORM-COMPARISON-0.1" + assert ( + report["declaration"]["mechanism_id"] + == "PLATFORM-COMPARISON-TEST-SUBJECT-1" + ) + assert "universal portability" in " ".join(report["limitations"]) + assert "record identical values" in report["claim_boundary"] + + +def test_generic_example_serializes_platform_neutral_output_bytes( + tmp_path: Path, +) -> None: + input_path = tmp_path / "input.txt" + output_path = tmp_path / "output.json" + metrics_path = tmp_path / "metrics.json" + input_path.write_text("beta alpha beta\n", encoding="utf-8", newline="\n") + + completed = subprocess.run( + [ + sys.executable, + str(ROOT / "examples" / "generic_run" / "compute.py"), + str(input_path), + str(output_path), + str(metrics_path), + ], + capture_output=True, + check=False, + ) + + assert completed.returncode == 0 + assert b"\r\n" not in output_path.read_bytes() + assert b"\r\n" not in metrics_path.read_bytes() + + +def test_continuous_integration_aggregate_cannot_mask_comparison_failure() -> None: + workflow = (ROOT / ".github" / "workflows" / "ci.yml").read_text( + encoding="utf-8" + ) + marker = "- name: Require comparable recorded results across all declared platforms" + step = workflow.split(marker, 1)[1].split(" - uses:", 1)[0] + + assert "| tee platform-comparison.json" not in step + assert "--json > platform-comparison.json" in step + assert 'report.get("status") == "PASS"' in step + assert 'report.get("exit_code") == 0' in step diff --git a/tests/test_presentation_surface.py b/tests/test_presentation_surface.py index b1ac0f5..8dc8427 100644 --- a/tests/test_presentation_surface.py +++ b/tests/test_presentation_surface.py @@ -520,6 +520,58 @@ def test_branch_coverage_is_retained_without_a_global_threshold() -> None: assert "coverage" in jobs["conformance-gate"]["needs"] +def test_cross_platform_comparability_uses_three_native_operating_systems() -> None: + workflow = yaml.safe_load( + (ROOT / ".github" / "workflows" / "ci.yml").read_text(encoding="utf-8") + ) + jobs = workflow["jobs"] + release_platforms = set(jobs["release-integrity"]["strategy"]["matrix"]["os"]) + assert release_platforms == { + "ubuntu-latest", + "windows-latest", + "macos-latest", + } + + observation = jobs["platform-comparability-observation"] + runner_platforms = { + (item["runner"], item["platform"]) + for item in observation["strategy"]["matrix"]["include"] + } + assert runner_platforms == { + ("ubuntu-latest", "Linux"), + ("windows-latest", "Windows"), + ("macos-15-intel", "Darwin"), + } + observation_setup = next( + step + for step in observation["steps"] + if str(step.get("uses", "")).startswith("actions/setup-python@") + ) + assert observation_setup["with"]["python-version"] == "3.12.10" + observation_commands = "\n".join( + str(step.get("run", "")) for step in observation["steps"] + ) + assert "platform.system()" in observation_commands + assert "vstd validate" in observation_commands + assert "vstd reproduce" in observation_commands + + aggregate = jobs["platform-comparability"] + aggregate_commands = "\n".join( + str(step.get("run", "")) for step in aggregate["steps"] + ) + assert aggregate["needs"] == ["platform-comparability-observation"] + aggregate_setup = next( + step + for step in aggregate["steps"] + if str(step.get("uses", "")).startswith("actions/setup-python@") + ) + assert aggregate_setup["with"]["python-version"] == "3.12.10" + assert "vstd compare-platforms" in aggregate_commands + assert "platform-comparison.json" in aggregate_commands + assert "platform-comparability-observation" in jobs["conformance-gate"]["needs"] + assert "platform-comparability" in jobs["conformance-gate"]["needs"] + + def test_pages_builder_refuses_to_merge_into_existing_content(tmp_path: Path) -> None: path = ROOT / "scripts/build_pages.py" spec = importlib.util.spec_from_file_location("build_pages_safety", path) diff --git a/tests/test_public_api.py b/tests/test_public_api.py index da8ccfd..e7653ad 100644 --- a/tests/test_public_api.py +++ b/tests/test_public_api.py @@ -23,6 +23,8 @@ "MechanismDecision", "MechanismOutcome", "ObligationCoordinate", + "PlatformComparisonResult", + "PlatformComparisonStatus", "ProvenanceHypergraph", "ReproducibilityLevel", "VerificationSession", @@ -37,6 +39,7 @@ "capture_run", "certificate_from_canonical_bytes", "compute_canonical_digest", + "compare_platform_run_receipts", "claim_binding_from_dict", "establish_graph_level", "establish_vstd4", diff --git a/tests/test_public_cli.py b/tests/test_public_cli.py index d9a3a31..a9676cf 100644 --- a/tests/test_public_cli.py +++ b/tests/test_public_cli.py @@ -48,6 +48,10 @@ def test_public_parser_has_no_target_specific_generation_commands() -> None: assert parser.parse_args(["data", "export", "receipt.json"]).data_command == "export" assert parser.parse_args(["plan", "manifest.json"]).command == "plan" assert parser.parse_args(["demo"]).command == "demo" + assert ( + parser.parse_args(["compare-platforms", "linux", "windows", "macos"]).command + == "compare-platforms" + ) assert ( parser.parse_args(["artifact", "verify", "bundle"]).artifact_command == "verify"